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.
@@ -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 };