wawesome 0.22.0 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets-Cgw3_rAq.mjs +336 -0
- package/dist/index.mjs +124 -398
- package/dist/local-modules-DvEInmfO.mjs +225 -0
- package/dist/local-modules.d.mts +16 -0
- package/dist/local-modules.mjs +2 -0
- package/dist/vite.d.mts +8 -2
- package/dist/vite.mjs +53 -3
- package/dist/vitest-setup.mjs +7 -0
- package/package.json +6 -2
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import crypto from "node:crypto";
|
|
5
|
+
//#region src/errors.ts
|
|
6
|
+
/**
|
|
7
|
+
* A rejection carrying the gateway's own prose and, where given, its reason.
|
|
8
|
+
*
|
|
9
|
+
* The call's trace id rides in the message itself, on a line of its own, so
|
|
10
|
+
* every place that prints a refusal prints the one thing support can search by.
|
|
11
|
+
*/
|
|
12
|
+
var GatewayError = class extends Error {
|
|
13
|
+
status;
|
|
14
|
+
reason;
|
|
15
|
+
/** The raw response, for `--verbose` to print when the prose is not enough. */
|
|
16
|
+
body;
|
|
17
|
+
traceId;
|
|
18
|
+
/** When a `locked` rename refusal says the next rename is allowed. */
|
|
19
|
+
renamableAt;
|
|
20
|
+
constructor(message, status, reason, body = "", traceId, renamableAt) {
|
|
21
|
+
super(traceId ? `${message}\n${traceLine(traceId)}` : message);
|
|
22
|
+
this.name = "GatewayError";
|
|
23
|
+
this.status = status;
|
|
24
|
+
this.reason = reason;
|
|
25
|
+
this.body = body;
|
|
26
|
+
this.traceId = traceId;
|
|
27
|
+
this.renamableAt = renamableAt;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
function traceLine(traceId) {
|
|
31
|
+
return `Trace ID: ${traceId}`;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Read a failed response into a `GatewayError`.
|
|
35
|
+
*
|
|
36
|
+
* `fallback` is used only when the response carries no prose of its own — a
|
|
37
|
+
* proxy's HTML error page, or an empty body. A 401 is deliberately *not* special
|
|
38
|
+
* cased here: what to say about an expired session depends on what the caller
|
|
39
|
+
* was doing, so callers handle it before reaching this.
|
|
40
|
+
*/
|
|
41
|
+
async function asGatewayError(res, fallback) {
|
|
42
|
+
const traceId = res.headers?.get("x-wawesome-trace-id") ?? void 0;
|
|
43
|
+
return rejectionOf(await res.text().catch(() => ""), res.status, fallback, traceId);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The same reading, for a caller that has already taken the body off the wire.
|
|
47
|
+
* `traceId` is the header's, for a body that never made it here as JSON.
|
|
48
|
+
*/
|
|
49
|
+
function rejectionOf(body, status, fallback, traceId) {
|
|
50
|
+
try {
|
|
51
|
+
const parsed = JSON.parse(body);
|
|
52
|
+
return new GatewayError(parsed.error || fallback, status, parsed.reason, body, parsed.trace_id || traceId, parsed.renamable_at);
|
|
53
|
+
} catch {
|
|
54
|
+
return new GatewayError(fallback, status, void 0, body, traceId);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** Why the gateway refused, where what was thrown might not be a refusal at all. */
|
|
58
|
+
function reasonOf(err) {
|
|
59
|
+
return err instanceof GatewayError ? err.reason : void 0;
|
|
60
|
+
}
|
|
61
|
+
/** What went wrong, as text, whatever was thrown. */
|
|
62
|
+
function errorText(err) {
|
|
63
|
+
return err instanceof Error ? err.message : String(err);
|
|
64
|
+
}
|
|
65
|
+
//#endregion
|
|
66
|
+
//#region src/credential.ts
|
|
67
|
+
const CREDENTIAL_ENV_VAR = "WAWESOME_DEPLOY_CREDENTIAL";
|
|
68
|
+
const SECRET_PREFIX = "wawe_";
|
|
69
|
+
function credentialFromEnvironment() {
|
|
70
|
+
const secret = process.env[CREDENTIAL_ENV_VAR]?.trim();
|
|
71
|
+
return secret ? secret : null;
|
|
72
|
+
}
|
|
73
|
+
/** Enough to match a credential against the one in a CI settings page, and no more. */
|
|
74
|
+
function credentialPrefix(secret) {
|
|
75
|
+
return secret.slice(0, 11);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* A credential has no session behind it, so the platform's own reason — unknown,
|
|
79
|
+
* revoked, expired, or a scope it does not carry — is the whole answer, and
|
|
80
|
+
* "sign in again" names a door a CI runner cannot open.
|
|
81
|
+
*/
|
|
82
|
+
function unauthorizedMessage(status, body) {
|
|
83
|
+
const secret = credentialFromEnvironment();
|
|
84
|
+
if (!secret) {
|
|
85
|
+
const refusal = rejectionOf(body, status, `The platform refused this request (HTTP ${status}).`);
|
|
86
|
+
if (status !== 401) return refusal.message;
|
|
87
|
+
const expired = "Authentication expired. Run 'wawesome login' again.";
|
|
88
|
+
return refusal.traceId ? `${expired}\n${traceLine(refusal.traceId)}` : expired;
|
|
89
|
+
}
|
|
90
|
+
return rejectionOf(body, status, `The deploy credential ${credentialPrefix(secret)} was refused (HTTP ${status}).`).message;
|
|
91
|
+
}
|
|
92
|
+
function reportSessionCommandRefused(secret, consequence) {
|
|
93
|
+
console.error(`[wawesome] Error: ${CREDENTIAL_ENV_VAR} is set, so this CLI authenticates as deploy credential ${credentialPrefix(secret)}.`);
|
|
94
|
+
console.error(`[wawesome] ${consequence} Unset ${CREDENTIAL_ENV_VAR} first.`);
|
|
95
|
+
}
|
|
96
|
+
//#endregion
|
|
97
|
+
//#region src/config.ts
|
|
98
|
+
/** Path to the user-level settings file (~/.wawesome/settings.json) */
|
|
99
|
+
function getSettingsPath() {
|
|
100
|
+
return path.join(os.homedir(), ".wawesome", "settings.json");
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Read stored user settings from ~/.wawesome/settings.json.
|
|
104
|
+
*/
|
|
105
|
+
function readSettings() {
|
|
106
|
+
const settingsPath = getSettingsPath();
|
|
107
|
+
if (!fs.existsSync(settingsPath)) return {};
|
|
108
|
+
try {
|
|
109
|
+
const raw = fs.readFileSync(settingsPath, "utf-8");
|
|
110
|
+
return JSON.parse(raw);
|
|
111
|
+
} catch {
|
|
112
|
+
return {};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const GATEWAY_ENV_NAMES = ["GATEWAY_URL", "WAWESOME_GATEWAY_URL"];
|
|
116
|
+
const DASHBOARD_ENV_NAMES = ["DASHBOARD_URL", "WAWESOME_DASHBOARD_URL"];
|
|
117
|
+
const GATEWAY_URL_FALLBACK = "https://api.wawesome.io";
|
|
118
|
+
const DASHBOARD_URL_FALLBACK = "https://dashboard.wawesome.io";
|
|
119
|
+
function resolveAddress(overrideUrl, envNames, settingsKey, stored, fallback) {
|
|
120
|
+
if (overrideUrl) return overrideUrl;
|
|
121
|
+
for (const name of envNames) {
|
|
122
|
+
const fromEnv = process.env[name];
|
|
123
|
+
if (fromEnv) return fromEnv;
|
|
124
|
+
}
|
|
125
|
+
const configured = readSettings()[settingsKey];
|
|
126
|
+
if (configured && typeof configured === "string") return configured;
|
|
127
|
+
return stored || fallback;
|
|
128
|
+
}
|
|
129
|
+
function getGatewayUrl(overrideUrl) {
|
|
130
|
+
return resolveAddress(overrideUrl, GATEWAY_ENV_NAMES, "gateway_url", readStoredCredentials()?.gateway_url, GATEWAY_URL_FALLBACK);
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* A tenant token is only valid at the gateway that issued it, so every other
|
|
134
|
+
* command has to follow the credentials. A login holds no token yet, and
|
|
135
|
+
* reading that rung here made a local gateway outlive the removal of every
|
|
136
|
+
* documented override, since only `logout` clears it. A **Deploy credential**
|
|
137
|
+
* reads the same rungs, and for a sharper reason: following a stale file would
|
|
138
|
+
* present it at a gateway that never minted it.
|
|
139
|
+
*/
|
|
140
|
+
function resolveGatewayUrlWithoutSession(overrideUrl) {
|
|
141
|
+
return resolveAddress(overrideUrl, GATEWAY_ENV_NAMES, "gateway_url", void 0, GATEWAY_URL_FALLBACK);
|
|
142
|
+
}
|
|
143
|
+
getGatewayUrl();
|
|
144
|
+
function getDashboardUrl(overrideUrl) {
|
|
145
|
+
return resolveAddress(overrideUrl, DASHBOARD_ENV_NAMES, "dashboard_url", readStoredCredentials()?.dashboard_url, DASHBOARD_URL_FALLBACK);
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* The dashboard for the environment this login is going to.
|
|
149
|
+
*
|
|
150
|
+
* Taken from the address the platform handed back to open, so a login that
|
|
151
|
+
* configures nothing stores the dashboard belonging to the gateway it signed
|
|
152
|
+
* into. An explicit flag, `DASHBOARD_URL` or a settings file still outrank it,
|
|
153
|
+
* on the rung they outrank every other address on.
|
|
154
|
+
*/
|
|
155
|
+
function resolveDashboardUrlForLogin(overrideUrl, signInUrl) {
|
|
156
|
+
return resolveAddress(overrideUrl, DASHBOARD_ENV_NAMES, "dashboard_url", originOf(signInUrl), DASHBOARD_URL_FALLBACK);
|
|
157
|
+
}
|
|
158
|
+
/** Undefined rather than throwing: an unreadable address falls to the rung below. */
|
|
159
|
+
function originOf(url) {
|
|
160
|
+
try {
|
|
161
|
+
return new URL(url).origin;
|
|
162
|
+
} catch {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
const LOGIN_CALLBACK_PORT = 9999;
|
|
167
|
+
/** Path to the user-level credentials file */
|
|
168
|
+
function getCredentialsPath() {
|
|
169
|
+
return path.join(os.homedir(), ".wawesome", "credentials.json");
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* What this process authenticates with: the **Deploy credential** in the
|
|
173
|
+
* environment where one is set, and otherwise the session `login` left on disk.
|
|
174
|
+
*
|
|
175
|
+
* The environment wins outright rather than filling a gap, so a CI runner
|
|
176
|
+
* carrying a stale cached home directory still authenticates as itself.
|
|
177
|
+
*/
|
|
178
|
+
function readCredentials() {
|
|
179
|
+
const secret = credentialFromEnvironment();
|
|
180
|
+
return secret ? environmentCredential(secret) : readStoredCredentials();
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* A credential wears the shape of a session so that every command reaches the
|
|
184
|
+
* platform the same way. What it does not carry is the point: no refresh token,
|
|
185
|
+
* because there is nothing behind it to renew, and no Tenant, because the
|
|
186
|
+
* platform resolves that from the credential.
|
|
187
|
+
*/
|
|
188
|
+
function environmentCredential(secret) {
|
|
189
|
+
return {
|
|
190
|
+
gateway_url: resolveGatewayUrlWithoutSession(void 0),
|
|
191
|
+
tenant_jwt: secret,
|
|
192
|
+
credential_prefix: credentialPrefix(secret)
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
/** The session on disk, whatever the environment says. */
|
|
196
|
+
function readStoredCredentials() {
|
|
197
|
+
const credPath = getCredentialsPath();
|
|
198
|
+
if (!fs.existsSync(credPath)) return null;
|
|
199
|
+
try {
|
|
200
|
+
const raw = fs.readFileSync(credPath, "utf-8");
|
|
201
|
+
return JSON.parse(raw);
|
|
202
|
+
} catch {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Write credentials to disk, creating the directory if needed.
|
|
208
|
+
*
|
|
209
|
+
* Owner-only, because the file holds a refresh token. The `chmod` is what
|
|
210
|
+
* tightens a file an older CLI left world-readable; a mode passed to
|
|
211
|
+
* `writeFileSync` applies only where it creates the file.
|
|
212
|
+
*/
|
|
213
|
+
function writeCredentials(creds) {
|
|
214
|
+
if (credentialFromEnvironment()) return;
|
|
215
|
+
const credPath = getCredentialsPath();
|
|
216
|
+
const dir = path.dirname(credPath);
|
|
217
|
+
fs.mkdirSync(dir, {
|
|
218
|
+
recursive: true,
|
|
219
|
+
mode: 448
|
|
220
|
+
});
|
|
221
|
+
fs.writeFileSync(credPath, JSON.stringify(creds, null, 2), {
|
|
222
|
+
encoding: "utf-8",
|
|
223
|
+
mode: 384
|
|
224
|
+
});
|
|
225
|
+
fs.chmodSync(credPath, 384);
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Delete stored credentials.
|
|
229
|
+
*/
|
|
230
|
+
function deleteCredentials() {
|
|
231
|
+
const credPath = getCredentialsPath();
|
|
232
|
+
if (fs.existsSync(credPath)) {
|
|
233
|
+
fs.rmSync(credPath, { force: true });
|
|
234
|
+
return true;
|
|
235
|
+
}
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
function getFunctionConfigPath(projectDir) {
|
|
239
|
+
return path.join(projectDir || process.cwd(), "wawesome-function.json");
|
|
240
|
+
}
|
|
241
|
+
function readFunctionConfig(projectDir) {
|
|
242
|
+
const configPath = getFunctionConfigPath(projectDir);
|
|
243
|
+
if (!fs.existsSync(configPath)) return null;
|
|
244
|
+
try {
|
|
245
|
+
const raw = fs.readFileSync(configPath, "utf-8");
|
|
246
|
+
return JSON.parse(raw);
|
|
247
|
+
} catch {
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
//#endregion
|
|
252
|
+
//#region src/assets.ts
|
|
253
|
+
/**
|
|
254
|
+
* The files under `dir`, hashed.
|
|
255
|
+
*
|
|
256
|
+
* Hashes are read off disk in a stream rather than by reading each file whole:
|
|
257
|
+
* a client build's largest chunk is not something to hold in memory just to
|
|
258
|
+
* find out the platform already has it.
|
|
259
|
+
*/
|
|
260
|
+
function collectAssets(dir) {
|
|
261
|
+
return listFiles(dir).map((file) => ({
|
|
262
|
+
...file,
|
|
263
|
+
content_hash: hashFile(file.source)
|
|
264
|
+
}));
|
|
265
|
+
}
|
|
266
|
+
/** The files a deploy of `dir` carries, before any is read. */
|
|
267
|
+
function listFiles(dir) {
|
|
268
|
+
if (!fs.existsSync(dir)) return [];
|
|
269
|
+
const files = [];
|
|
270
|
+
const walk = (current, prefix) => {
|
|
271
|
+
for (const entry of fs.readdirSync(current, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
272
|
+
const absolute = path.join(current, entry.name);
|
|
273
|
+
const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
274
|
+
if (entry.isDirectory()) walk(absolute, relative);
|
|
275
|
+
else if (entry.isFile()) files.push({
|
|
276
|
+
path: relative,
|
|
277
|
+
size_bytes: fs.statSync(absolute).size,
|
|
278
|
+
source: absolute
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
walk(dir, "");
|
|
283
|
+
return files;
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* A **Static asset** a browser renders as a page. Read off the extension, as the
|
|
287
|
+
* content type the platform records is — mirrors `content_type_for_path` in
|
|
288
|
+
* `server/crates/core/src/features/assets/content_type.rs`.
|
|
289
|
+
*/
|
|
290
|
+
function isDocument(assetPath) {
|
|
291
|
+
const name = assetPath.split("/").pop() ?? "";
|
|
292
|
+
const dot = name.lastIndexOf(".");
|
|
293
|
+
const extension = dot === -1 ? "" : name.slice(dot + 1).toLowerCase();
|
|
294
|
+
return extension === "html" || extension === "htm";
|
|
295
|
+
}
|
|
296
|
+
function hashFile(file) {
|
|
297
|
+
return sha256(fs.readFileSync(file));
|
|
298
|
+
}
|
|
299
|
+
function sha256(bytes) {
|
|
300
|
+
return crypto.createHash("sha256").update(bytes).digest("hex");
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* What this deploy *is*: the server bundle together with the sorted set of
|
|
304
|
+
* asset path and content-hash pairs, the same set for its **Private file**s
|
|
305
|
+
* marked apart, and the **Fallback document** it declares.
|
|
306
|
+
*
|
|
307
|
+
* Computed here, before anything is uploaded, so a deploy that changed nothing
|
|
308
|
+
* is refused before a byte moves. Mirrors `deploy_digest` in
|
|
309
|
+
* `server/crates/core/src/features/assets/identity.rs` — the gateway recomputes
|
|
310
|
+
* it from what actually arrives, so the two have to agree exactly.
|
|
311
|
+
*
|
|
312
|
+
* A deploy declaring no fallback and no private files contributes nothing here
|
|
313
|
+
* for either rather than a stated absence, so every digest computed before
|
|
314
|
+
* they existed is unmoved.
|
|
315
|
+
*/
|
|
316
|
+
const DEPLOY_DIGEST_DOMAIN = "wawesome-deploy-v1";
|
|
317
|
+
function deployDigest(bundle, assets, fallback, privateFiles = []) {
|
|
318
|
+
const byPath = (files) => [...files].sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
319
|
+
const digest = crypto.createHash("sha256");
|
|
320
|
+
digest.update(`${DEPLOY_DIGEST_DOMAIN}\n`);
|
|
321
|
+
digest.update(`${sha256(bundle)}\n`);
|
|
322
|
+
for (const asset of byPath(assets)) digest.update(`${asset.path}\0${asset.content_hash}\n`);
|
|
323
|
+
for (const file of byPath(privateFiles)) digest.update(`private\0${file.path}\0${file.content_hash}\n`);
|
|
324
|
+
if (fallback) digest.update(`fallback\0${fallback.path}\0${fallback.status}\n`);
|
|
325
|
+
return digest.digest("hex");
|
|
326
|
+
}
|
|
327
|
+
/** The manifest as the gateway reads it — the bytes on disk are the CLI's business. */
|
|
328
|
+
function manifestOf(assets) {
|
|
329
|
+
return assets.map(({ path, content_hash, size_bytes }) => ({
|
|
330
|
+
path,
|
|
331
|
+
content_hash,
|
|
332
|
+
size_bytes
|
|
333
|
+
}));
|
|
334
|
+
}
|
|
335
|
+
//#endregion
|
|
336
|
+
export { asGatewayError as C, rejectionOf as E, GatewayError as S, reasonOf as T, SECRET_PREFIX as _, manifestOf as a, reportSessionCommandRefused as b, getDashboardUrl as c, readCredentials as d, readFunctionConfig as f, CREDENTIAL_ENV_VAR as g, writeCredentials as h, listFiles as i, getFunctionConfigPath as l, resolveGatewayUrlWithoutSession as m, deployDigest as n, LOGIN_CALLBACK_PORT as o, resolveDashboardUrlForLogin as p, isDocument as r, deleteCredentials as s, collectAssets as t, getGatewayUrl as u, credentialFromEnvironment as v, errorText as w, unauthorizedMessage as x, credentialPrefix as y };
|