@xtrable-ltd/nanoesis 0.1.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/LICENSE +21 -0
- package/README.md +73 -0
- package/dist/adapter-azure-blob.d.ts +97 -0
- package/dist/adapter-azure-blob.js +127 -0
- package/dist/adapter-cloudflare.d.ts +28 -0
- package/dist/adapter-cloudflare.js +32 -0
- package/dist/adapter-fs.d.ts +38 -0
- package/dist/adapter-fs.js +54 -0
- package/dist/adapter-local-jwt.d.ts +205 -0
- package/dist/adapter-local-jwt.js +550 -0
- package/dist/adapter-sharp.d.ts +11 -0
- package/dist/adapter-sharp.js +39 -0
- package/dist/adapter-shell.d.ts +48 -0
- package/dist/adapter-shell.js +56 -0
- package/dist/adapter-trusted-header.d.ts +43 -0
- package/dist/adapter-trusted-header.js +21 -0
- package/dist/chunk-G2UEZTYC.js +2541 -0
- package/dist/editor-api.d.ts +198 -0
- package/dist/editor-api.js +592 -0
- package/dist/editor.d.ts +13 -0
- package/dist/editor.js +6 -0
- package/dist/index.d.ts +1238 -0
- package/dist/index.js +124 -0
- package/editor/assets/TemplatesPane-5qsDAK_B.js +792 -0
- package/editor/assets/TemplatesPane-B4_sg2u5.css +1 -0
- package/editor/assets/abap-BrgZPUOV.js +6 -0
- package/editor/assets/apex-DyP6w7ZV.js +6 -0
- package/editor/assets/azcli-BaLxmfj-.js +6 -0
- package/editor/assets/bat-CFOPXBzS.js +6 -0
- package/editor/assets/bicep-BfEKNvv3.js +7 -0
- package/editor/assets/cameligo-BFG1Mk7z.js +6 -0
- package/editor/assets/clojure-DTECt2xU.js +6 -0
- package/editor/assets/codicon-DCmgc-ay.ttf +0 -0
- package/editor/assets/coffee-CDGzqUPQ.js +6 -0
- package/editor/assets/cpp-CLLBncYj.js +6 -0
- package/editor/assets/csharp-dUCx_-0o.js +6 -0
- package/editor/assets/csp-5Rap-vPy.js +6 -0
- package/editor/assets/css-D3h14YRZ.js +8 -0
- package/editor/assets/css.worker-DaIe3gwK.js +84 -0
- package/editor/assets/cssMode-CGp4MIjR.js +9 -0
- package/editor/assets/cypher-DrQuvNYM.js +6 -0
- package/editor/assets/dart-CFKIUWau.js +6 -0
- package/editor/assets/dockerfile-Zznr-cwX.js +6 -0
- package/editor/assets/ecl-Ce3n6wWz.js +6 -0
- package/editor/assets/editor.worker-BCzxt1at.js +12 -0
- package/editor/assets/elixir-deUWdS0T.js +6 -0
- package/editor/assets/flow9-i9-g7ZhI.js +6 -0
- package/editor/assets/freemarker2-CJkwxmPv.js +8 -0
- package/editor/assets/fsharp-CzKuDChf.js +6 -0
- package/editor/assets/go-Cphgjts3.js +6 -0
- package/editor/assets/graphql-Cg7bfA9N.js +6 -0
- package/editor/assets/handlebars-CKb5i2nM.js +6 -0
- package/editor/assets/hcl-0cvrggvQ.js +6 -0
- package/editor/assets/html-DyMbQx0w.js +6 -0
- package/editor/assets/html.worker-CKrFyw_2.js +461 -0
- package/editor/assets/htmlMode-DVPeqtn-.js +9 -0
- package/editor/assets/index-CbuWEnUB.css +7 -0
- package/editor/assets/index-DJmSgobK.js +129 -0
- package/editor/assets/ini-Drc7WvVn.js +6 -0
- package/editor/assets/java-B_fMsGYe.js +6 -0
- package/editor/assets/javascript-Bp1Qh9wR.js +6 -0
- package/editor/assets/json.worker-B7c_PmGb.js +49 -0
- package/editor/assets/jsonMode-FLEeVtx7.js +15 -0
- package/editor/assets/julia-Bqgm2twL.js +6 -0
- package/editor/assets/kotlin-BSkB5QuD.js +6 -0
- package/editor/assets/less-BsTHnhdd.js +7 -0
- package/editor/assets/lexon-YWi4-JPR.js +6 -0
- package/editor/assets/liquid-Bh8c534t.js +6 -0
- package/editor/assets/lua-nf6ki56Z.js +6 -0
- package/editor/assets/m3-Cpb6xl2v.js +6 -0
- package/editor/assets/markdown-DSZPf7rp.js +6 -0
- package/editor/assets/mdx-BUbo8M9l.js +6 -0
- package/editor/assets/mips-B_c3zf-v.js +6 -0
- package/editor/assets/msdax-rUNN04Wq.js +6 -0
- package/editor/assets/mysql-DDwshQtU.js +6 -0
- package/editor/assets/nanoesis-logo-CgieIWPg.png +0 -0
- package/editor/assets/objective-c-B5zXfXm9.js +6 -0
- package/editor/assets/pascal-CXOwvkN_.js +6 -0
- package/editor/assets/pascaligo-Bc-ZgV77.js +6 -0
- package/editor/assets/perl-CwNk8-XU.js +6 -0
- package/editor/assets/pgsql-tGk8EFnU.js +6 -0
- package/editor/assets/php-CpIb_Oan.js +6 -0
- package/editor/assets/pla-B03wrqEc.js +6 -0
- package/editor/assets/postiats-BKlk5iyT.js +6 -0
- package/editor/assets/powerquery-Bhzvs7bI.js +6 -0
- package/editor/assets/powershell-Dd3NCNK9.js +6 -0
- package/editor/assets/protobuf-COyEY5Pt.js +7 -0
- package/editor/assets/pug-BaJupSGV.js +6 -0
- package/editor/assets/python-CuJlk8g3.js +6 -0
- package/editor/assets/qsharp-DXyYeYxl.js +6 -0
- package/editor/assets/r-CdQndTaG.js +6 -0
- package/editor/assets/razor-CuQT_1Ku.js +6 -0
- package/editor/assets/redis-CVwtpugi.js +6 -0
- package/editor/assets/redshift-25W9uPmb.js +6 -0
- package/editor/assets/restructuredtext-DfzH4Xui.js +6 -0
- package/editor/assets/ruby-Cp1zYvxS.js +6 -0
- package/editor/assets/rust-D5C2fndG.js +6 -0
- package/editor/assets/sb-CDntyWJ8.js +6 -0
- package/editor/assets/scala-BoFRg7Ot.js +6 -0
- package/editor/assets/scheme-Bio4gycK.js +6 -0
- package/editor/assets/scss-4Ik7cdeQ.js +8 -0
- package/editor/assets/shell-CX-rkNHf.js +6 -0
- package/editor/assets/solidity-Tw7wswEv.js +6 -0
- package/editor/assets/sophia-C5WLch3f.js +6 -0
- package/editor/assets/sparql-DHaeiCBh.js +6 -0
- package/editor/assets/sql-CCSDG5nI.js +6 -0
- package/editor/assets/st-pnP8ivHi.js +6 -0
- package/editor/assets/swift-DwJ7jVG9.js +8 -0
- package/editor/assets/systemverilog-B9Xyijhd.js +6 -0
- package/editor/assets/tcl-DnHyzjbg.js +6 -0
- package/editor/assets/ts.worker-BhkL8olL.js +51334 -0
- package/editor/assets/tsMode-CT2HUNtN.js +16 -0
- package/editor/assets/twig-CPajHgWi.js +6 -0
- package/editor/assets/typescript-CtMx97cn.js +6 -0
- package/editor/assets/typespec-D-MeaMDU.js +6 -0
- package/editor/assets/vb-DgyLZaXg.js +6 -0
- package/editor/assets/wgsl-BIv9DU6q.js +303 -0
- package/editor/assets/xml-CyfpINj_.js +6 -0
- package/editor/assets/yaml-BBWmgfMA.js +6 -0
- package/editor/config.json +3 -0
- package/editor/index.html +28 -0
- package/package.json +85 -0
|
@@ -0,0 +1,550 @@
|
|
|
1
|
+
// ../../adapters/local-jwt/src/user-store.ts
|
|
2
|
+
import { mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
3
|
+
import { dirname } from "path";
|
|
4
|
+
var InMemoryUserStore = class {
|
|
5
|
+
byId = /* @__PURE__ */ new Map();
|
|
6
|
+
async isEmpty() {
|
|
7
|
+
return this.byId.size === 0;
|
|
8
|
+
}
|
|
9
|
+
async getByUsername(username) {
|
|
10
|
+
const target = username.toLowerCase();
|
|
11
|
+
for (const user of this.byId.values()) {
|
|
12
|
+
if (user.username.toLowerCase() === target) return user;
|
|
13
|
+
}
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
async getById(id) {
|
|
17
|
+
return this.byId.get(id) ?? null;
|
|
18
|
+
}
|
|
19
|
+
async put(user) {
|
|
20
|
+
this.byId.set(user.id, user);
|
|
21
|
+
}
|
|
22
|
+
async remove(id) {
|
|
23
|
+
this.byId.delete(id);
|
|
24
|
+
}
|
|
25
|
+
async list() {
|
|
26
|
+
return [...this.byId.values()].sort((a, b) => a.username.localeCompare(b.username));
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
var FileUserStore = class {
|
|
30
|
+
constructor(path) {
|
|
31
|
+
this.path = path;
|
|
32
|
+
}
|
|
33
|
+
path;
|
|
34
|
+
async load() {
|
|
35
|
+
try {
|
|
36
|
+
const raw = await readFile(this.path, "utf8");
|
|
37
|
+
const text = raw.charCodeAt(0) === 65279 ? raw.slice(1) : raw;
|
|
38
|
+
const parsed = JSON.parse(text);
|
|
39
|
+
return Array.isArray(parsed.users) ? parsed.users : [];
|
|
40
|
+
} catch (error) {
|
|
41
|
+
if (error.code === "ENOENT") return [];
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async save(users) {
|
|
46
|
+
await mkdir(dirname(this.path), { recursive: true });
|
|
47
|
+
const tmp = `${this.path}.tmp`;
|
|
48
|
+
await writeFile(tmp, JSON.stringify({ users }, null, 2), "utf8");
|
|
49
|
+
await rename(tmp, this.path);
|
|
50
|
+
}
|
|
51
|
+
async isEmpty() {
|
|
52
|
+
const users = await this.load();
|
|
53
|
+
return users.length === 0;
|
|
54
|
+
}
|
|
55
|
+
async getByUsername(username) {
|
|
56
|
+
const target = username.toLowerCase();
|
|
57
|
+
const users = await this.load();
|
|
58
|
+
return users.find((u) => u.username.toLowerCase() === target) ?? null;
|
|
59
|
+
}
|
|
60
|
+
async getById(id) {
|
|
61
|
+
const users = await this.load();
|
|
62
|
+
return users.find((u) => u.id === id) ?? null;
|
|
63
|
+
}
|
|
64
|
+
async put(user) {
|
|
65
|
+
const users = await this.load();
|
|
66
|
+
const idx = users.findIndex((u) => u.id === user.id);
|
|
67
|
+
if (idx >= 0) users[idx] = user;
|
|
68
|
+
else users.push(user);
|
|
69
|
+
await this.save(users);
|
|
70
|
+
}
|
|
71
|
+
async remove(id) {
|
|
72
|
+
const users = await this.load();
|
|
73
|
+
const next = users.filter((u) => u.id !== id);
|
|
74
|
+
if (next.length !== users.length) await this.save(next);
|
|
75
|
+
}
|
|
76
|
+
async list() {
|
|
77
|
+
const users = await this.load();
|
|
78
|
+
return [...users].sort((a, b) => a.username.localeCompare(b.username));
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// ../../adapters/local-jwt/src/password.ts
|
|
83
|
+
import { randomBytes, scrypt, timingSafeEqual } from "crypto";
|
|
84
|
+
import { promisify } from "util";
|
|
85
|
+
var scryptAsync = promisify(scrypt);
|
|
86
|
+
var SALT_BYTES = 16;
|
|
87
|
+
var HASH_BYTES = 64;
|
|
88
|
+
var SCRYPT_OPTIONS = { N: 1 << 15, r: 8, p: 1, maxmem: 64 * 1024 * 1024 };
|
|
89
|
+
var VERSION = "scrypt$1";
|
|
90
|
+
var WeakPasswordError = class extends Error {
|
|
91
|
+
constructor(reason) {
|
|
92
|
+
super(reason);
|
|
93
|
+
this.name = "WeakPasswordError";
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
async function hashPassword(password) {
|
|
97
|
+
if (password === "") {
|
|
98
|
+
throw new WeakPasswordError("password is required");
|
|
99
|
+
}
|
|
100
|
+
const salt = randomBytes(SALT_BYTES);
|
|
101
|
+
const derived = await scryptAsync(password, salt, HASH_BYTES, SCRYPT_OPTIONS);
|
|
102
|
+
return `${VERSION}$${salt.toString("base64")}$${derived.toString("base64")}`;
|
|
103
|
+
}
|
|
104
|
+
async function verifyPassword(password, encoded) {
|
|
105
|
+
const parts = encoded.split("$");
|
|
106
|
+
if (parts.length !== 4 || `${parts[0]}$${parts[1]}` !== VERSION) return false;
|
|
107
|
+
const salt = Buffer.from(parts[2], "base64");
|
|
108
|
+
const expected = Buffer.from(parts[3], "base64");
|
|
109
|
+
if (salt.length === 0 || expected.length === 0) return false;
|
|
110
|
+
let derived;
|
|
111
|
+
try {
|
|
112
|
+
derived = await scryptAsync(password, salt, expected.length, SCRYPT_OPTIONS);
|
|
113
|
+
} catch {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
if (derived.length !== expected.length) return false;
|
|
117
|
+
return timingSafeEqual(derived, expected);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ../../adapters/local-jwt/src/jwt.ts
|
|
121
|
+
import { createHmac, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
122
|
+
var HEADER = { alg: "HS256", typ: "JWT" };
|
|
123
|
+
var HEADER_B64 = base64UrlEncode(JSON.stringify(HEADER));
|
|
124
|
+
function signJwt(payload, secret) {
|
|
125
|
+
const body = base64UrlEncode(JSON.stringify(payload));
|
|
126
|
+
const signingInput = `${HEADER_B64}.${body}`;
|
|
127
|
+
const signature = base64UrlBuffer(hmac(secret, signingInput));
|
|
128
|
+
return `${signingInput}.${signature}`;
|
|
129
|
+
}
|
|
130
|
+
function verifyJwt(token, secret, nowSeconds = Math.floor(Date.now() / 1e3)) {
|
|
131
|
+
const parts = token.split(".");
|
|
132
|
+
if (parts.length !== 3) return { ok: false, reason: "malformed" };
|
|
133
|
+
const [header, body, signature] = parts;
|
|
134
|
+
if (header !== HEADER_B64) return { ok: false, reason: "malformed" };
|
|
135
|
+
const expected = hmac(secret, `${header}.${body}`);
|
|
136
|
+
const got = base64UrlDecode(signature);
|
|
137
|
+
if (got.length !== expected.length || !timingSafeEqual2(got, expected)) {
|
|
138
|
+
return { ok: false, reason: "bad-signature" };
|
|
139
|
+
}
|
|
140
|
+
let payload;
|
|
141
|
+
try {
|
|
142
|
+
const parsed = JSON.parse(base64UrlDecode(body).toString("utf8"));
|
|
143
|
+
if (!isJwtPayload(parsed)) return { ok: false, reason: "malformed" };
|
|
144
|
+
payload = parsed;
|
|
145
|
+
} catch {
|
|
146
|
+
return { ok: false, reason: "malformed" };
|
|
147
|
+
}
|
|
148
|
+
if (payload.exp <= nowSeconds) return { ok: false, reason: "expired" };
|
|
149
|
+
return { ok: true, payload };
|
|
150
|
+
}
|
|
151
|
+
function hmac(secret, data) {
|
|
152
|
+
return createHmac("sha256", secret).update(data).digest();
|
|
153
|
+
}
|
|
154
|
+
function base64UrlEncode(s) {
|
|
155
|
+
return Buffer.from(s, "utf8").toString("base64").replace(/=+$/, "").replace(/\+/g, "-").replace(/\//g, "_");
|
|
156
|
+
}
|
|
157
|
+
function base64UrlBuffer(b) {
|
|
158
|
+
return b.toString("base64").replace(/=+$/, "").replace(/\+/g, "-").replace(/\//g, "_");
|
|
159
|
+
}
|
|
160
|
+
function base64UrlDecode(s) {
|
|
161
|
+
const padded = s + "=".repeat((4 - s.length % 4) % 4);
|
|
162
|
+
return Buffer.from(padded.replace(/-/g, "+").replace(/_/g, "/"), "base64");
|
|
163
|
+
}
|
|
164
|
+
function isJwtPayload(value) {
|
|
165
|
+
if (typeof value !== "object" || value === null) return false;
|
|
166
|
+
const v = value;
|
|
167
|
+
return typeof v["sub"] === "string" && Array.isArray(v["roles"]) && v["roles"].every((r) => typeof r === "string") && typeof v["lc"] === "number" && typeof v["iat"] === "number" && typeof v["exp"] === "number";
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ../../adapters/local-jwt/src/local-jwt.ts
|
|
171
|
+
import { randomUUID } from "crypto";
|
|
172
|
+
var BEARER_PREFIX = /^Bearer\s+/i;
|
|
173
|
+
function createLocalJwtAuth(config) {
|
|
174
|
+
const ttl = config.accessTokenTtlSeconds ?? 600;
|
|
175
|
+
const patTtl = config.patTtlSeconds ?? 60 * 60 * 24 * 90;
|
|
176
|
+
const refreshGrace = config.refreshGraceSeconds ?? 60 * 60 * 24 * 30;
|
|
177
|
+
const maxFails = config.maxFailedAttempts ?? 3;
|
|
178
|
+
const lockoutMs = (config.lockoutSeconds ?? 60) * 1e3;
|
|
179
|
+
const now = config.now ?? Date.now;
|
|
180
|
+
const newId = config.newId ?? randomUUID;
|
|
181
|
+
const users = config.users;
|
|
182
|
+
function extractBearer(getHeader) {
|
|
183
|
+
const raw = getHeader("authorization") ?? getHeader("Authorization");
|
|
184
|
+
if (raw === void 0) return void 0;
|
|
185
|
+
const match = BEARER_PREFIX.exec(raw);
|
|
186
|
+
return match ? raw.slice(match[0].length).trim() : void 0;
|
|
187
|
+
}
|
|
188
|
+
function toPrincipal(user) {
|
|
189
|
+
return {
|
|
190
|
+
userId: user.id,
|
|
191
|
+
username: user.username,
|
|
192
|
+
roles: user.roles
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
function issueToken(user) {
|
|
196
|
+
const iat = Math.floor(now() / 1e3);
|
|
197
|
+
const payload = {
|
|
198
|
+
sub: user.id,
|
|
199
|
+
roles: user.roles,
|
|
200
|
+
lc: user.loginCount,
|
|
201
|
+
iat,
|
|
202
|
+
exp: iat + ttl
|
|
203
|
+
};
|
|
204
|
+
return signJwt(payload, config.signingSecret);
|
|
205
|
+
}
|
|
206
|
+
function toSummary(user) {
|
|
207
|
+
return {
|
|
208
|
+
id: user.id,
|
|
209
|
+
username: user.username,
|
|
210
|
+
...user.displayName !== void 0 && { displayName: user.displayName },
|
|
211
|
+
roles: user.roles,
|
|
212
|
+
createdAt: user.createdAt,
|
|
213
|
+
updatedAt: user.updatedAt,
|
|
214
|
+
locked: user.lockedUntil !== void 0 && user.lockedUntil > now()
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
function normalizeDisplayName(raw) {
|
|
218
|
+
if (raw === void 0) return void 0;
|
|
219
|
+
const trimmed = raw.trim();
|
|
220
|
+
return trimmed === "" ? void 0 : trimmed;
|
|
221
|
+
}
|
|
222
|
+
const VALID_ROLES = ["author", "developer", "admin"];
|
|
223
|
+
function isValidRoles(roles) {
|
|
224
|
+
return Array.isArray(roles) && roles.length > 0 && roles.every((role) => VALID_ROLES.includes(role));
|
|
225
|
+
}
|
|
226
|
+
function sameRoles(a, b) {
|
|
227
|
+
if (a.length !== b.length) return false;
|
|
228
|
+
const sorted = [...b].sort();
|
|
229
|
+
return [...a].sort().every((role, index) => role === sorted[index]);
|
|
230
|
+
}
|
|
231
|
+
async function adminCount() {
|
|
232
|
+
const all = await users.list();
|
|
233
|
+
return all.filter((user) => user.roles.includes("admin")).length;
|
|
234
|
+
}
|
|
235
|
+
const identity = {
|
|
236
|
+
async authenticate(getHeader) {
|
|
237
|
+
const token = extractBearer(getHeader);
|
|
238
|
+
if (token === void 0) return null;
|
|
239
|
+
const result = verifyJwt(token, config.signingSecret, Math.floor(now() / 1e3));
|
|
240
|
+
if (!result.ok) return null;
|
|
241
|
+
const user = await users.getById(result.payload.sub);
|
|
242
|
+
if (user === null) return null;
|
|
243
|
+
if (result.payload.pat === true) {
|
|
244
|
+
if ((user.patVersion ?? 0) !== (result.payload.pv ?? 0)) return null;
|
|
245
|
+
} else if (user.loginCount !== result.payload.lc) {
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
return toPrincipal(user);
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
async function consumeToken(token, graceSeconds = 0) {
|
|
252
|
+
const nowSeconds = Math.floor(now() / 1e3);
|
|
253
|
+
const result = verifyJwt(token, config.signingSecret, nowSeconds - graceSeconds);
|
|
254
|
+
if (!result.ok) return null;
|
|
255
|
+
const user = await users.getById(result.payload.sub);
|
|
256
|
+
if (user === null) return null;
|
|
257
|
+
if (user.loginCount !== result.payload.lc) return null;
|
|
258
|
+
return user;
|
|
259
|
+
}
|
|
260
|
+
const endpoints = {
|
|
261
|
+
async login({ username, password }) {
|
|
262
|
+
if (typeof username !== "string" || typeof password !== "string") {
|
|
263
|
+
return { ok: false, status: 400, error: "username and password are required" };
|
|
264
|
+
}
|
|
265
|
+
if (username.trim() === "" || password === "") {
|
|
266
|
+
return { ok: false, status: 400, error: "username and password are required" };
|
|
267
|
+
}
|
|
268
|
+
if (await users.isEmpty()) {
|
|
269
|
+
const created = {
|
|
270
|
+
id: newId(),
|
|
271
|
+
username: username.trim(),
|
|
272
|
+
roles: ["admin"],
|
|
273
|
+
passwordHash: await hashPassword(password),
|
|
274
|
+
loginCount: 1,
|
|
275
|
+
failedAttempts: 0,
|
|
276
|
+
createdAt: now(),
|
|
277
|
+
updatedAt: now()
|
|
278
|
+
};
|
|
279
|
+
await users.put(created);
|
|
280
|
+
return {
|
|
281
|
+
ok: true,
|
|
282
|
+
value: {
|
|
283
|
+
token: issueToken(created),
|
|
284
|
+
principal: toPrincipal(created),
|
|
285
|
+
firstRunBootstrap: true
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
const user = await users.getByUsername(username);
|
|
290
|
+
if (user === null) {
|
|
291
|
+
return { ok: false, status: 401, error: "invalid credentials" };
|
|
292
|
+
}
|
|
293
|
+
if (user.lockedUntil !== void 0 && user.lockedUntil > now()) {
|
|
294
|
+
return { ok: false, status: 423, error: "account temporarily locked" };
|
|
295
|
+
}
|
|
296
|
+
const passwordOk = await verifyPassword(password, user.passwordHash);
|
|
297
|
+
if (!passwordOk) {
|
|
298
|
+
const failedAttempts = user.failedAttempts + 1;
|
|
299
|
+
const next2 = {
|
|
300
|
+
...user,
|
|
301
|
+
failedAttempts,
|
|
302
|
+
...failedAttempts >= maxFails && { lockedUntil: now() + lockoutMs },
|
|
303
|
+
updatedAt: now()
|
|
304
|
+
};
|
|
305
|
+
await users.put(next2);
|
|
306
|
+
return { ok: false, status: 401, error: "invalid credentials" };
|
|
307
|
+
}
|
|
308
|
+
const next = {
|
|
309
|
+
id: user.id,
|
|
310
|
+
username: user.username,
|
|
311
|
+
...user.displayName !== void 0 && { displayName: user.displayName },
|
|
312
|
+
roles: user.roles,
|
|
313
|
+
passwordHash: user.passwordHash,
|
|
314
|
+
loginCount: user.loginCount + 1,
|
|
315
|
+
failedAttempts: 0,
|
|
316
|
+
...user.patVersion !== void 0 && { patVersion: user.patVersion },
|
|
317
|
+
createdAt: user.createdAt,
|
|
318
|
+
updatedAt: now()
|
|
319
|
+
};
|
|
320
|
+
await users.put(next);
|
|
321
|
+
return {
|
|
322
|
+
ok: true,
|
|
323
|
+
value: {
|
|
324
|
+
token: issueToken(next),
|
|
325
|
+
principal: toPrincipal(next),
|
|
326
|
+
firstRunBootstrap: false
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
},
|
|
330
|
+
async refresh(token) {
|
|
331
|
+
const user = await consumeToken(token, refreshGrace);
|
|
332
|
+
if (user === null) {
|
|
333
|
+
return { ok: false, status: 401, error: "invalid or expired token" };
|
|
334
|
+
}
|
|
335
|
+
return { ok: true, value: { token: issueToken(user), principal: toPrincipal(user) } };
|
|
336
|
+
},
|
|
337
|
+
async logout(token) {
|
|
338
|
+
const user = await consumeToken(token);
|
|
339
|
+
if (user !== null) {
|
|
340
|
+
const next = {
|
|
341
|
+
...user,
|
|
342
|
+
loginCount: user.loginCount + 1,
|
|
343
|
+
updatedAt: now()
|
|
344
|
+
};
|
|
345
|
+
await users.put(next);
|
|
346
|
+
}
|
|
347
|
+
return { ok: true, value: { ok: true } };
|
|
348
|
+
},
|
|
349
|
+
async state() {
|
|
350
|
+
return { firstRun: await users.isEmpty() };
|
|
351
|
+
},
|
|
352
|
+
async changePassword(userId, { currentPassword, newPassword }) {
|
|
353
|
+
const user = await users.getById(userId);
|
|
354
|
+
if (user === null) {
|
|
355
|
+
return { ok: false, status: 401, error: "invalid credentials" };
|
|
356
|
+
}
|
|
357
|
+
if (typeof currentPassword !== "string" || typeof newPassword !== "string") {
|
|
358
|
+
return { ok: false, status: 400, error: "currentPassword and newPassword are required" };
|
|
359
|
+
}
|
|
360
|
+
if (newPassword === "") {
|
|
361
|
+
return { ok: false, status: 400, error: "password is required" };
|
|
362
|
+
}
|
|
363
|
+
if (!await verifyPassword(currentPassword, user.passwordHash)) {
|
|
364
|
+
return { ok: false, status: 401, error: "invalid credentials" };
|
|
365
|
+
}
|
|
366
|
+
const next = {
|
|
367
|
+
id: user.id,
|
|
368
|
+
username: user.username,
|
|
369
|
+
...user.displayName !== void 0 && { displayName: user.displayName },
|
|
370
|
+
roles: user.roles,
|
|
371
|
+
passwordHash: await hashPassword(newPassword),
|
|
372
|
+
loginCount: user.loginCount + 1,
|
|
373
|
+
failedAttempts: 0,
|
|
374
|
+
...user.patVersion !== void 0 && { patVersion: user.patVersion },
|
|
375
|
+
createdAt: user.createdAt,
|
|
376
|
+
updatedAt: now()
|
|
377
|
+
};
|
|
378
|
+
await users.put(next);
|
|
379
|
+
return { ok: true, value: { token: issueToken(next) } };
|
|
380
|
+
},
|
|
381
|
+
async createToken(userId) {
|
|
382
|
+
const user = await users.getById(userId);
|
|
383
|
+
if (user === null) return { ok: false, status: 404, error: "no such user" };
|
|
384
|
+
const iat = Math.floor(now() / 1e3);
|
|
385
|
+
const exp = iat + patTtl;
|
|
386
|
+
const payload = {
|
|
387
|
+
sub: user.id,
|
|
388
|
+
roles: user.roles,
|
|
389
|
+
lc: user.loginCount,
|
|
390
|
+
iat,
|
|
391
|
+
exp,
|
|
392
|
+
pat: true,
|
|
393
|
+
pv: user.patVersion ?? 0
|
|
394
|
+
};
|
|
395
|
+
return {
|
|
396
|
+
ok: true,
|
|
397
|
+
value: { token: signJwt(payload, config.signingSecret), expiresAt: exp * 1e3 }
|
|
398
|
+
};
|
|
399
|
+
},
|
|
400
|
+
async revokeTokens(userId) {
|
|
401
|
+
const user = await users.getById(userId);
|
|
402
|
+
if (user === null) return { ok: false, status: 404, error: "no such user" };
|
|
403
|
+
const next = {
|
|
404
|
+
id: user.id,
|
|
405
|
+
username: user.username,
|
|
406
|
+
roles: user.roles,
|
|
407
|
+
passwordHash: user.passwordHash,
|
|
408
|
+
loginCount: user.loginCount,
|
|
409
|
+
failedAttempts: user.failedAttempts,
|
|
410
|
+
...user.lockedUntil !== void 0 && { lockedUntil: user.lockedUntil },
|
|
411
|
+
patVersion: (user.patVersion ?? 0) + 1,
|
|
412
|
+
createdAt: user.createdAt,
|
|
413
|
+
updatedAt: now()
|
|
414
|
+
};
|
|
415
|
+
await users.put(next);
|
|
416
|
+
return { ok: true, value: { ok: true } };
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
const userAdmin = {
|
|
420
|
+
async listUsers() {
|
|
421
|
+
const all = await users.list();
|
|
422
|
+
return { ok: true, value: all.map(toSummary) };
|
|
423
|
+
},
|
|
424
|
+
async createUser({
|
|
425
|
+
username,
|
|
426
|
+
password,
|
|
427
|
+
roles,
|
|
428
|
+
displayName
|
|
429
|
+
}) {
|
|
430
|
+
if (typeof username !== "string" || username.trim() === "") {
|
|
431
|
+
return { ok: false, status: 400, error: "username is required" };
|
|
432
|
+
}
|
|
433
|
+
if (typeof password !== "string" || password === "") {
|
|
434
|
+
return { ok: false, status: 400, error: "password is required" };
|
|
435
|
+
}
|
|
436
|
+
if (!isValidRoles(roles)) {
|
|
437
|
+
return {
|
|
438
|
+
ok: false,
|
|
439
|
+
status: 400,
|
|
440
|
+
error: "roles must be a non-empty list of author|developer|admin"
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
const handle = username.trim();
|
|
444
|
+
if (await users.getByUsername(handle) !== null) {
|
|
445
|
+
return { ok: false, status: 409, error: "username already exists" };
|
|
446
|
+
}
|
|
447
|
+
const cleanName = normalizeDisplayName(displayName);
|
|
448
|
+
const created = {
|
|
449
|
+
id: newId(),
|
|
450
|
+
username: handle,
|
|
451
|
+
...cleanName !== void 0 && { displayName: cleanName },
|
|
452
|
+
roles: [...roles],
|
|
453
|
+
passwordHash: await hashPassword(password),
|
|
454
|
+
loginCount: 0,
|
|
455
|
+
// no session issued at create; first login bumps to 1 and mints the token
|
|
456
|
+
failedAttempts: 0,
|
|
457
|
+
createdAt: now(),
|
|
458
|
+
updatedAt: now()
|
|
459
|
+
};
|
|
460
|
+
await users.put(created);
|
|
461
|
+
return { ok: true, value: toSummary(created) };
|
|
462
|
+
},
|
|
463
|
+
async updateUser(callerId, targetId, { roles, displayName }) {
|
|
464
|
+
const user = await users.getById(targetId);
|
|
465
|
+
if (user === null) return { ok: false, status: 404, error: "no such user" };
|
|
466
|
+
const nextDisplayName = displayName === void 0 ? user.displayName : normalizeDisplayName(displayName);
|
|
467
|
+
let nextRoles = user.roles;
|
|
468
|
+
let rolesChanged = false;
|
|
469
|
+
if (roles !== void 0) {
|
|
470
|
+
if (!isValidRoles(roles)) {
|
|
471
|
+
return {
|
|
472
|
+
ok: false,
|
|
473
|
+
status: 400,
|
|
474
|
+
error: "roles must be a non-empty list of author|developer|admin"
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
if (!sameRoles(user.roles, roles)) {
|
|
478
|
+
rolesChanged = true;
|
|
479
|
+
if (targetId === callerId) {
|
|
480
|
+
return { ok: false, status: 403, error: "cannot change your own role" };
|
|
481
|
+
}
|
|
482
|
+
const demotingAdmin = user.roles.includes("admin") && !roles.includes("admin");
|
|
483
|
+
if (demotingAdmin && await adminCount() <= 1) {
|
|
484
|
+
return { ok: false, status: 409, error: "cannot demote the last admin" };
|
|
485
|
+
}
|
|
486
|
+
nextRoles = [...roles];
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
const next = {
|
|
490
|
+
id: user.id,
|
|
491
|
+
username: user.username,
|
|
492
|
+
...nextDisplayName !== void 0 && { displayName: nextDisplayName },
|
|
493
|
+
roles: nextRoles,
|
|
494
|
+
passwordHash: user.passwordHash,
|
|
495
|
+
loginCount: rolesChanged ? user.loginCount + 1 : user.loginCount,
|
|
496
|
+
failedAttempts: user.failedAttempts,
|
|
497
|
+
...user.lockedUntil !== void 0 && { lockedUntil: user.lockedUntil },
|
|
498
|
+
...user.patVersion !== void 0 && { patVersion: user.patVersion },
|
|
499
|
+
createdAt: user.createdAt,
|
|
500
|
+
updatedAt: now()
|
|
501
|
+
};
|
|
502
|
+
await users.put(next);
|
|
503
|
+
return { ok: true, value: toSummary(next) };
|
|
504
|
+
},
|
|
505
|
+
async resetPassword(targetId, { newPassword }) {
|
|
506
|
+
const user = await users.getById(targetId);
|
|
507
|
+
if (user === null) return { ok: false, status: 404, error: "no such user" };
|
|
508
|
+
if (typeof newPassword !== "string" || newPassword === "") {
|
|
509
|
+
return { ok: false, status: 400, error: "password is required" };
|
|
510
|
+
}
|
|
511
|
+
const next = {
|
|
512
|
+
id: user.id,
|
|
513
|
+
username: user.username,
|
|
514
|
+
...user.displayName !== void 0 && { displayName: user.displayName },
|
|
515
|
+
roles: user.roles,
|
|
516
|
+
passwordHash: await hashPassword(newPassword),
|
|
517
|
+
loginCount: user.loginCount + 1,
|
|
518
|
+
failedAttempts: 0,
|
|
519
|
+
...user.patVersion !== void 0 && { patVersion: user.patVersion },
|
|
520
|
+
createdAt: user.createdAt,
|
|
521
|
+
updatedAt: now()
|
|
522
|
+
};
|
|
523
|
+
await users.put(next);
|
|
524
|
+
return { ok: true, value: { ok: true } };
|
|
525
|
+
},
|
|
526
|
+
async deleteUser(callerId, targetId) {
|
|
527
|
+
const user = await users.getById(targetId);
|
|
528
|
+
if (user === null) return { ok: false, status: 404, error: "no such user" };
|
|
529
|
+
if (targetId === callerId) {
|
|
530
|
+
return { ok: false, status: 403, error: "cannot delete your own account" };
|
|
531
|
+
}
|
|
532
|
+
if (user.roles.includes("admin") && await adminCount() <= 1) {
|
|
533
|
+
return { ok: false, status: 409, error: "cannot delete the last admin" };
|
|
534
|
+
}
|
|
535
|
+
await users.remove(targetId);
|
|
536
|
+
return { ok: true, value: { ok: true } };
|
|
537
|
+
}
|
|
538
|
+
};
|
|
539
|
+
return { identity, endpoints, userAdmin };
|
|
540
|
+
}
|
|
541
|
+
export {
|
|
542
|
+
FileUserStore,
|
|
543
|
+
InMemoryUserStore,
|
|
544
|
+
WeakPasswordError,
|
|
545
|
+
createLocalJwtAuth,
|
|
546
|
+
hashPassword,
|
|
547
|
+
signJwt,
|
|
548
|
+
verifyJwt,
|
|
549
|
+
verifyPassword
|
|
550
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ImageEncoder } from '@nanoesis/engine';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The sharp (libvips) implementation of the engine's {@link ImageEncoder} port, the
|
|
5
|
+
* only place that touches native image code, kept out of the engine so it stays pure
|
|
6
|
+
* (CLAUDE.md §2/§3). Used by every host that publishes (the Azure Function host and
|
|
7
|
+
* host-mcp). sharp ships prebuilt binaries, so no compiler or admin rights are needed.
|
|
8
|
+
*/
|
|
9
|
+
declare function createSharpEncoder(): ImageEncoder;
|
|
10
|
+
|
|
11
|
+
export { createSharpEncoder };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// ../../adapters/sharp/src/sharp-encoder.ts
|
|
2
|
+
import sharp from "sharp";
|
|
3
|
+
function createSharpEncoder() {
|
|
4
|
+
return {
|
|
5
|
+
async encode(input, request) {
|
|
6
|
+
const buffer = Buffer.from(input);
|
|
7
|
+
const metadata = await sharp(buffer).metadata();
|
|
8
|
+
const sourceWidth = metadata.width ?? 0;
|
|
9
|
+
const sourceHeight = metadata.height ?? 0;
|
|
10
|
+
const widths = [.../* @__PURE__ */ new Set([...request.widths, sourceWidth])].filter((width) => width > 0 && width <= sourceWidth).sort((a, b) => a - b);
|
|
11
|
+
const variants = [];
|
|
12
|
+
for (const format of request.formats) {
|
|
13
|
+
for (const width of widths) {
|
|
14
|
+
const bytes = await encodeOne(buffer, width, format);
|
|
15
|
+
variants.push({ format, width, bytes });
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
const blur = await sharp(buffer).resize({ width: 16 }).webp({ quality: 40 }).toBuffer();
|
|
19
|
+
const blurDataUri = `data:image/webp;base64,${blur.toString("base64")}`;
|
|
20
|
+
return { sourceWidth, sourceHeight, variants, blurDataUri };
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function encodeOne(buffer, width, format) {
|
|
25
|
+
const pipeline = sharp(buffer).resize({ width, withoutEnlargement: true });
|
|
26
|
+
switch (format) {
|
|
27
|
+
case "avif":
|
|
28
|
+
return pipeline.avif({ quality: 50 }).toBuffer();
|
|
29
|
+
case "webp":
|
|
30
|
+
return pipeline.webp({ quality: 75 }).toBuffer();
|
|
31
|
+
case "png":
|
|
32
|
+
return pipeline.png().toBuffer();
|
|
33
|
+
case "jpeg":
|
|
34
|
+
return pipeline.jpeg({ quality: 78, mozjpeg: true }).toBuffer();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export {
|
|
38
|
+
createSharpEncoder
|
|
39
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { PreBuildHook } from '@nanoesis/engine';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A spawner seam, kept narrow so tests can substitute it without spawning real
|
|
5
|
+
* processes. Resolves with the exit code (or `null` when the child was killed by
|
|
6
|
+
* a signal). The default implementation uses `node:child_process.spawn` with
|
|
7
|
+
* `shell: true` so dev-friendly commands like `npm run build:css` Just Work.
|
|
8
|
+
*/
|
|
9
|
+
type Spawner = (command: string, options: {
|
|
10
|
+
readonly cwd: string;
|
|
11
|
+
readonly env: NodeJS.ProcessEnv;
|
|
12
|
+
}) => Promise<number | null>;
|
|
13
|
+
interface ShellPreBuildHookConfig {
|
|
14
|
+
/** The command line to run, exactly as a developer would type it in a terminal. */
|
|
15
|
+
readonly command: string;
|
|
16
|
+
/** Working directory the command runs in (typically the site root). */
|
|
17
|
+
readonly cwd: string;
|
|
18
|
+
/** Environment for the child; defaults to the host's own `process.env`. */
|
|
19
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
20
|
+
/** Injected for tests; defaults to the real `child_process.spawn`. */
|
|
21
|
+
readonly spawn?: Spawner;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* A {@link PreBuildHook} that runs a shell command before compile (DESIGN §11, the
|
|
25
|
+
* spot where a dev's own asset tool, e.g. Tailwind, can rebuild into `public/` so
|
|
26
|
+
* its output is picked up by the engine's passthrough). The child inherits stdio
|
|
27
|
+
* so logs stream live; a non-zero exit code throws, which aborts the publish and
|
|
28
|
+
* leaves the sink untouched (the engine's contract, see `publishSite`).
|
|
29
|
+
*/
|
|
30
|
+
declare class ShellPreBuildHook implements PreBuildHook {
|
|
31
|
+
private readonly config;
|
|
32
|
+
private readonly spawnFn;
|
|
33
|
+
constructor(config: ShellPreBuildHookConfig);
|
|
34
|
+
run(): Promise<void>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Read the optional `nanoesis.prebuild` command from a site's package.json.
|
|
39
|
+
* Returns undefined when the file is missing, the JSON is malformed, the
|
|
40
|
+
* `nanoesis` key is absent, or the command is empty whitespace, anything that
|
|
41
|
+
* means "no prebuild here". A non-empty string command is returned verbatim.
|
|
42
|
+
*
|
|
43
|
+
* Hosts (the Azure Function host, host-mcp, …) read the same key, so a
|
|
44
|
+
* site declares its build hook in one canonical place.
|
|
45
|
+
*/
|
|
46
|
+
declare function readPrebuildCommand(siteDir: string): Promise<string | undefined>;
|
|
47
|
+
|
|
48
|
+
export { ShellPreBuildHook, type ShellPreBuildHookConfig, type Spawner, readPrebuildCommand };
|