hanoman 0.1.22 → 0.1.24
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/build-info.json +3 -3
- package/dist/cli.js +24 -1
- package/dist/server.js +239 -23
- package/docs/agent-integration.md +4 -0
- package/package.json +1 -1
- package/prisma/migrations/20260810000000_client_portal_access/migration.sql +15 -0
- package/prisma/schema.prisma +25 -1
- package/web/assets/{index-BXg9KRmB.css → index-CnAQHtuX.css} +1 -1
- package/web/assets/{index-Di1yjym7.js → index-DqY-l_Cl.js} +1543 -1543
- package/web/index.html +2 -2
package/dist/build-info.json
CHANGED
package/dist/cli.js
CHANGED
|
@@ -4986,7 +4986,7 @@ var init_prd_status = __esm({
|
|
|
4986
4986
|
});
|
|
4987
4987
|
|
|
4988
4988
|
// ../shared/src/dto.ts
|
|
4989
|
-
var zIssueDeviceToken, zSessionResult, zSessionHistory, zCreateProject, zProjectId, zRenameProject, zUpdateProject, zCreateSpec, zPatchSpec, zChangeSpecSource, zIntegrate, zSessionSummary, zProjectView, zSchedulerQueueItem, zSchedulerSourceView, zSchedulerSessionView, zSchedulerQueueCounts, zSchedulerState, zLeadDecisionView, zLeadFlowView, zLeadAnswer, zLeadProjectStatus, zLeadStatusView, zFlow, zPrdBrief, zPrdDoc, zBreakdownItem, zBreakdownDoc, zBatchCreateSpec, zEscalationTarget, zEscalationPrefill, zAuditEscalation, zAuditEscalationView, zTerminalSession, zDocFileContent, zDocIndexCat, zDocIndex, HOST_RE, USER_RE, zCreateVps, zPatchVps, zLogin, zSignup, zChangePassword, zVpsCheck, zMarkNa, zMarkNaBulk, zAttest, zRemediate, UPDATE_RESTART_EXIT, zUpdateApplyBody, zTicketView, zTicketAttachmentView, zTicketDetail, zTicketEditInput, zHelpInfo, zPublicTicketStatus;
|
|
4989
|
+
var zIssueDeviceToken, zSessionResult, zSessionHistory, zCreateProject, zProjectId, zRenameProject, zUpdateProject, zCreateSpec, zPatchSpec, zChangeSpecSource, zIntegrate, zSessionSummary, zProjectView, zSchedulerQueueItem, zSchedulerSourceView, zSchedulerSessionView, zSchedulerQueueCounts, zSchedulerState, zLeadDecisionView, zLeadFlowView, zLeadAnswer, zLeadProjectStatus, zLeadStatusView, zFlow, zPrdBrief, zPrdDoc, zBreakdownItem, zBreakdownDoc, zBatchCreateSpec, zEscalationTarget, zEscalationPrefill, zAuditEscalation, zAuditEscalationView, zTerminalSession, zDocFileContent, zDocIndexCat, zDocIndex, HOST_RE, USER_RE, zCreateVps, zPatchVps, USER_ROLES, zUserRole, zLogin, zSignup, zChangePassword, zCreateClientAccount, zUpdateClientAccount, zVpsCheck, zMarkNa, zMarkNaBulk, zAttest, zRemediate, UPDATE_RESTART_EXIT, zUpdateApplyBody, zTicketView, zTicketAttachmentView, zTicketDetail, zTicketEditInput, zHelpInfo, zPublicTicketStatus;
|
|
4990
4990
|
var init_dto = __esm({
|
|
4991
4991
|
"../shared/src/dto.ts"() {
|
|
4992
4992
|
"use strict";
|
|
@@ -5400,12 +5400,24 @@ var init_dto = __esm({
|
|
|
5400
5400
|
password: external_exports.string().min(1)
|
|
5401
5401
|
// SPEC-165 · diisi = bootstrap ulang
|
|
5402
5402
|
}).partial();
|
|
5403
|
+
USER_ROLES = ["admin", "client"];
|
|
5404
|
+
zUserRole = external_exports.enum(USER_ROLES);
|
|
5403
5405
|
zLogin = external_exports.object({ email: external_exports.string().email(), password: external_exports.string().min(1) });
|
|
5404
5406
|
zSignup = external_exports.object({ email: external_exports.string().email(), password: external_exports.string().min(8) });
|
|
5405
5407
|
zChangePassword = external_exports.object({
|
|
5406
5408
|
currentPassword: external_exports.string().min(1),
|
|
5407
5409
|
newPassword: external_exports.string().min(8)
|
|
5408
5410
|
});
|
|
5411
|
+
zCreateClientAccount = external_exports.object({
|
|
5412
|
+
email: external_exports.string().email(),
|
|
5413
|
+
password: external_exports.string().min(8),
|
|
5414
|
+
projects: external_exports.array(external_exports.string().min(1)).default([])
|
|
5415
|
+
});
|
|
5416
|
+
zUpdateClientAccount = external_exports.object({
|
|
5417
|
+
projects: external_exports.array(external_exports.string().min(1)),
|
|
5418
|
+
disabled: external_exports.boolean(),
|
|
5419
|
+
password: external_exports.string().min(8)
|
|
5420
|
+
}).partial();
|
|
5409
5421
|
zVpsCheck = external_exports.object({
|
|
5410
5422
|
check: external_exports.string(),
|
|
5411
5423
|
status: external_exports.enum(["pass", "fail", "warn", "na"]),
|
|
@@ -5741,6 +5753,14 @@ var init_ticket_status = __esm({
|
|
|
5741
5753
|
}
|
|
5742
5754
|
});
|
|
5743
5755
|
|
|
5756
|
+
// ../shared/src/portal.ts
|
|
5757
|
+
var init_portal = __esm({
|
|
5758
|
+
"../shared/src/portal.ts"() {
|
|
5759
|
+
"use strict";
|
|
5760
|
+
init_ticket_status();
|
|
5761
|
+
}
|
|
5762
|
+
});
|
|
5763
|
+
|
|
5744
5764
|
// ../shared/src/session-kind.ts
|
|
5745
5765
|
var SESSION_KINDS, zSessionKind;
|
|
5746
5766
|
var init_session_kind = __esm({
|
|
@@ -7022,6 +7042,7 @@ var init_src = __esm({
|
|
|
7022
7042
|
init_api();
|
|
7023
7043
|
init_coverage();
|
|
7024
7044
|
init_ticket_status();
|
|
7045
|
+
init_portal();
|
|
7025
7046
|
init_prd_status();
|
|
7026
7047
|
init_session_kind();
|
|
7027
7048
|
init_config();
|
|
@@ -28591,7 +28612,9 @@ var init_migrate_pg = __esm({
|
|
|
28591
28612
|
// SPEC-516 · ADR-0105 · Changelog sesudah Project (FK projectId). Tabel ini LOCAL-only dan
|
|
28592
28613
|
// lazimnya TIDAK ada di sumber Postgres lama — jalur 42P01 memperlakukannya sebagai nol baris.
|
|
28593
28614
|
"Changelog",
|
|
28615
|
+
// SPEC-617 · ADR-0110 · ClientProjectAccess sesudah User DAN Project (FK ke keduanya).
|
|
28594
28616
|
"User",
|
|
28617
|
+
"ClientProjectAccess",
|
|
28595
28618
|
"Session",
|
|
28596
28619
|
"DeviceToken",
|
|
28597
28620
|
"AgentToken",
|
package/dist/server.js
CHANGED
|
@@ -5237,7 +5237,7 @@ var init_prd_status = __esm({
|
|
|
5237
5237
|
function flowForSource(source) {
|
|
5238
5238
|
return source === "qa" ? "qa" : source === "audit" ? "audit" : source === "goal" ? "goal" : "feature";
|
|
5239
5239
|
}
|
|
5240
|
-
var zIssueDeviceToken, zSessionResult, zSessionHistory, zCreateProject, zProjectId, zRenameProject, zUpdateProject, zCreateSpec, zPatchSpec, zChangeSpecSource, zIntegrate, zSessionSummary, zProjectView, zSchedulerQueueItem, zSchedulerSourceView, zSchedulerSessionView, zSchedulerQueueCounts, zSchedulerState, zLeadDecisionView, zLeadFlowView, zLeadAnswer, zLeadProjectStatus, zLeadStatusView, zFlow, zPrdBrief, zPrdDoc, zBreakdownItem, zBreakdownDoc, zBatchCreateSpec, zEscalationTarget, zEscalationPrefill, zAuditEscalation, zAuditEscalationView, zTerminalSession, zDocFileContent, zDocIndexCat, zDocIndex, HOST_RE, USER_RE, zCreateVps, zPatchVps, zLogin, zSignup, zChangePassword, zVpsCheck, zMarkNa, zMarkNaBulk, zAttest, zRemediate, UPDATE_RESTART_EXIT, zUpdateApplyBody, zTicketView, zTicketAttachmentView, zTicketDetail, zTicketEditInput, zHelpInfo, zPublicTicketStatus;
|
|
5240
|
+
var zIssueDeviceToken, zSessionResult, zSessionHistory, zCreateProject, zProjectId, zRenameProject, zUpdateProject, zCreateSpec, zPatchSpec, zChangeSpecSource, zIntegrate, zSessionSummary, zProjectView, zSchedulerQueueItem, zSchedulerSourceView, zSchedulerSessionView, zSchedulerQueueCounts, zSchedulerState, zLeadDecisionView, zLeadFlowView, zLeadAnswer, zLeadProjectStatus, zLeadStatusView, zFlow, zPrdBrief, zPrdDoc, zBreakdownItem, zBreakdownDoc, zBatchCreateSpec, zEscalationTarget, zEscalationPrefill, zAuditEscalation, zAuditEscalationView, zTerminalSession, zDocFileContent, zDocIndexCat, zDocIndex, HOST_RE, USER_RE, zCreateVps, zPatchVps, USER_ROLES, zUserRole, zLogin, zSignup, zChangePassword, zCreateClientAccount, zUpdateClientAccount, zVpsCheck, zMarkNa, zMarkNaBulk, zAttest, zRemediate, UPDATE_RESTART_EXIT, zUpdateApplyBody, zTicketView, zTicketAttachmentView, zTicketDetail, zTicketEditInput, zHelpInfo, zPublicTicketStatus;
|
|
5241
5241
|
var init_dto = __esm({
|
|
5242
5242
|
"../shared/src/dto.ts"() {
|
|
5243
5243
|
"use strict";
|
|
@@ -5651,12 +5651,24 @@ var init_dto = __esm({
|
|
|
5651
5651
|
password: external_exports.string().min(1)
|
|
5652
5652
|
// SPEC-165 · diisi = bootstrap ulang
|
|
5653
5653
|
}).partial();
|
|
5654
|
+
USER_ROLES = ["admin", "client"];
|
|
5655
|
+
zUserRole = external_exports.enum(USER_ROLES);
|
|
5654
5656
|
zLogin = external_exports.object({ email: external_exports.string().email(), password: external_exports.string().min(1) });
|
|
5655
5657
|
zSignup = external_exports.object({ email: external_exports.string().email(), password: external_exports.string().min(8) });
|
|
5656
5658
|
zChangePassword = external_exports.object({
|
|
5657
5659
|
currentPassword: external_exports.string().min(1),
|
|
5658
5660
|
newPassword: external_exports.string().min(8)
|
|
5659
5661
|
});
|
|
5662
|
+
zCreateClientAccount = external_exports.object({
|
|
5663
|
+
email: external_exports.string().email(),
|
|
5664
|
+
password: external_exports.string().min(8),
|
|
5665
|
+
projects: external_exports.array(external_exports.string().min(1)).default([])
|
|
5666
|
+
});
|
|
5667
|
+
zUpdateClientAccount = external_exports.object({
|
|
5668
|
+
projects: external_exports.array(external_exports.string().min(1)),
|
|
5669
|
+
disabled: external_exports.boolean(),
|
|
5670
|
+
password: external_exports.string().min(8)
|
|
5671
|
+
}).partial();
|
|
5660
5672
|
zVpsCheck = external_exports.object({
|
|
5661
5673
|
check: external_exports.string(),
|
|
5662
5674
|
status: external_exports.enum(["pass", "fail", "warn", "na"]),
|
|
@@ -6002,6 +6014,44 @@ var init_ticket_status = __esm({
|
|
|
6002
6014
|
}
|
|
6003
6015
|
});
|
|
6004
6016
|
|
|
6017
|
+
// ../shared/src/portal.ts
|
|
6018
|
+
function toPortalProject(p) {
|
|
6019
|
+
return { id: p.id, name: p.name };
|
|
6020
|
+
}
|
|
6021
|
+
function toPortalSpec(s2) {
|
|
6022
|
+
return {
|
|
6023
|
+
id: s2.id,
|
|
6024
|
+
title: s2.title,
|
|
6025
|
+
priority: s2.priority,
|
|
6026
|
+
stage: s2.stage,
|
|
6027
|
+
objective: s2.objective,
|
|
6028
|
+
createdAt: iso(s2.createdAt),
|
|
6029
|
+
startedAt: iso(s2.startedAt),
|
|
6030
|
+
doneAt: iso(s2.doneAt)
|
|
6031
|
+
};
|
|
6032
|
+
}
|
|
6033
|
+
function toPortalTicket(t, specStage) {
|
|
6034
|
+
return {
|
|
6035
|
+
id: t.id,
|
|
6036
|
+
number: t.number,
|
|
6037
|
+
category: t.category,
|
|
6038
|
+
title: t.title,
|
|
6039
|
+
status: publicStatus(t.status, specStage),
|
|
6040
|
+
createdAt: iso(t.createdAt)
|
|
6041
|
+
};
|
|
6042
|
+
}
|
|
6043
|
+
function toPortalTicketDetail(t, specStage) {
|
|
6044
|
+
return { ...toPortalTicket(t, specStage), detail: t.detail };
|
|
6045
|
+
}
|
|
6046
|
+
var iso;
|
|
6047
|
+
var init_portal = __esm({
|
|
6048
|
+
"../shared/src/portal.ts"() {
|
|
6049
|
+
"use strict";
|
|
6050
|
+
init_ticket_status();
|
|
6051
|
+
iso = (d) => d == null ? null : typeof d === "string" ? d : d.toISOString();
|
|
6052
|
+
}
|
|
6053
|
+
});
|
|
6054
|
+
|
|
6005
6055
|
// ../shared/src/session-kind.ts
|
|
6006
6056
|
var SESSION_KINDS, zSessionKind;
|
|
6007
6057
|
var init_session_kind = __esm({
|
|
@@ -6315,14 +6365,14 @@ function matchesEvent(subscribed, type) {
|
|
|
6315
6365
|
}
|
|
6316
6366
|
function projectRow(def2, row) {
|
|
6317
6367
|
const out3 = {};
|
|
6318
|
-
for (const f of def2.fields) if (f in row) out3[f] =
|
|
6368
|
+
for (const f of def2.fields) if (f in row) out3[f] = iso2(row[f]);
|
|
6319
6369
|
return out3;
|
|
6320
6370
|
}
|
|
6321
6371
|
function diffFields(def2, before, after) {
|
|
6322
6372
|
const out3 = [];
|
|
6323
6373
|
for (const f of def2.fields) {
|
|
6324
6374
|
if (IGNORED_FIELDS.has(f)) continue;
|
|
6325
|
-
if (JSON.stringify(
|
|
6375
|
+
if (JSON.stringify(iso2(before[f]) ?? null) !== JSON.stringify(iso2(after[f]) ?? null)) out3.push(f);
|
|
6326
6376
|
}
|
|
6327
6377
|
return out3;
|
|
6328
6378
|
}
|
|
@@ -6361,7 +6411,7 @@ function clampEnvelope(env) {
|
|
|
6361
6411
|
}
|
|
6362
6412
|
return { ...env, truncated: true, truncatedFields: fields };
|
|
6363
6413
|
}
|
|
6364
|
-
var WEBHOOK_SPEC_VERSION, WEBHOOK_BACKOFF_SEC, WEBHOOK_MAX_ATTEMPTS, WEBHOOK_FAIL_LIMIT, WEBHOOK_QUEUE_CAP, WEBHOOK_HISTORY_KEEP, WEBHOOK_MAX_BYTES, WEBHOOK_FIELD_MAX_CHARS, WEBHOOK_TIMEOUT_MS, WEBHOOK_DEFAULT_PER_MINUTE, WEBHOOK_PING_TYPE, WEBHOOK_USER_AGENT, WEBHOOK_HEADERS, SYSTEM_ACTOR, IGNORED_FIELDS, WEBHOOK_ENTITIES, BY_MODEL, WEBHOOK_EVENTS, webhookEventTypes,
|
|
6414
|
+
var WEBHOOK_SPEC_VERSION, WEBHOOK_BACKOFF_SEC, WEBHOOK_MAX_ATTEMPTS, WEBHOOK_FAIL_LIMIT, WEBHOOK_QUEUE_CAP, WEBHOOK_HISTORY_KEEP, WEBHOOK_MAX_BYTES, WEBHOOK_FIELD_MAX_CHARS, WEBHOOK_TIMEOUT_MS, WEBHOOK_DEFAULT_PER_MINUTE, WEBHOOK_PING_TYPE, WEBHOOK_USER_AGENT, WEBHOOK_HEADERS, SYSTEM_ACTOR, IGNORED_FIELDS, WEBHOOK_ENTITIES, BY_MODEL, WEBHOOK_EVENTS, webhookEventTypes, iso2, zCreateWebhookEndpoint, zUpdateWebhookEndpoint;
|
|
6365
6415
|
var init_webhook = __esm({
|
|
6366
6416
|
"../shared/src/webhook.ts"() {
|
|
6367
6417
|
"use strict";
|
|
@@ -6718,7 +6768,7 @@ var init_webhook = __esm({
|
|
|
6718
6768
|
}
|
|
6719
6769
|
];
|
|
6720
6770
|
webhookEventTypes = () => WEBHOOK_EVENTS.map((e) => e.type);
|
|
6721
|
-
|
|
6771
|
+
iso2 = (v) => v instanceof Date ? v.toISOString() : v;
|
|
6722
6772
|
zCreateWebhookEndpoint = external_exports.object({
|
|
6723
6773
|
name: external_exports.string().trim().min(1).max(80),
|
|
6724
6774
|
url: external_exports.string().trim().min(1).max(2e3),
|
|
@@ -7381,6 +7431,7 @@ var init_src = __esm({
|
|
|
7381
7431
|
init_api();
|
|
7382
7432
|
init_coverage();
|
|
7383
7433
|
init_ticket_status();
|
|
7434
|
+
init_portal();
|
|
7384
7435
|
init_prd_status();
|
|
7385
7436
|
init_session_kind();
|
|
7386
7437
|
init_config();
|
|
@@ -29986,10 +30037,85 @@ async function webhooks_default(app2) {
|
|
|
29986
30037
|
});
|
|
29987
30038
|
}
|
|
29988
30039
|
|
|
29989
|
-
// src/
|
|
29990
|
-
|
|
30040
|
+
// src/routes/portal.ts
|
|
30041
|
+
init_src();
|
|
30042
|
+
init_db();
|
|
29991
30043
|
|
|
29992
|
-
// src/
|
|
30044
|
+
// src/services/client-access.ts
|
|
30045
|
+
init_db();
|
|
30046
|
+
var segments = (path) => path.replace(/^\/api\/?/, "").replace(/\/+$/, "").split("/").filter(Boolean);
|
|
30047
|
+
function clientRouteAllowed(method, path) {
|
|
30048
|
+
const seg = segments(path);
|
|
30049
|
+
if (seg.includes("..")) return false;
|
|
30050
|
+
const top = seg[0] ?? "";
|
|
30051
|
+
const read = method === "GET" || method === "HEAD";
|
|
30052
|
+
if (top === "portal") return read;
|
|
30053
|
+
if (top === "help") return true;
|
|
30054
|
+
if (top === "auth") {
|
|
30055
|
+
const sub = seg[1] ?? "";
|
|
30056
|
+
return method === "POST" && (sub === "logout" || sub === "change-password");
|
|
30057
|
+
}
|
|
30058
|
+
return false;
|
|
30059
|
+
}
|
|
30060
|
+
async function clientProjectIds(userId) {
|
|
30061
|
+
const rows = await prisma.clientProjectAccess.findMany({
|
|
30062
|
+
where: { userId },
|
|
30063
|
+
select: { projectId: true },
|
|
30064
|
+
orderBy: { projectId: "asc" }
|
|
30065
|
+
});
|
|
30066
|
+
return rows.map((r) => r.projectId);
|
|
30067
|
+
}
|
|
30068
|
+
async function hasProjectAccess(userId, projectId) {
|
|
30069
|
+
return await prisma.clientProjectAccess.count({ where: { userId, projectId } }) > 0;
|
|
30070
|
+
}
|
|
30071
|
+
|
|
30072
|
+
// src/routes/portal.ts
|
|
30073
|
+
var NOT_FOUND = { error: "not found" };
|
|
30074
|
+
async function specStages(ids) {
|
|
30075
|
+
const wanted = [...new Set(ids.filter((v) => !!v))];
|
|
30076
|
+
if (!wanted.length) return /* @__PURE__ */ new Map();
|
|
30077
|
+
const rows = await prisma.spec.findMany({ where: { id: { in: wanted } }, select: { id: true, stage: true } });
|
|
30078
|
+
return new Map(rows.map((r) => [r.id, r.stage]));
|
|
30079
|
+
}
|
|
30080
|
+
async function portal_default(app2) {
|
|
30081
|
+
app2.get("/portal/projects", async (req) => {
|
|
30082
|
+
const ids = await clientProjectIds(req.user.id);
|
|
30083
|
+
const rows = await prisma.project.findMany({ where: { id: { in: ids } }, orderBy: { name: "asc" } });
|
|
30084
|
+
return { items: rows.map(toPortalProject) };
|
|
30085
|
+
});
|
|
30086
|
+
app2.get("/portal/projects/:id/backlog", async (req, reply) => {
|
|
30087
|
+
const { id } = req.params;
|
|
30088
|
+
if (!await hasProjectAccess(req.user.id, id)) return reply.code(404).send(NOT_FOUND);
|
|
30089
|
+
const { page, limit } = req.query;
|
|
30090
|
+
const specs = await liveSpecs({ project: id });
|
|
30091
|
+
return paginate(specs.map(toPortalSpec), page, limit);
|
|
30092
|
+
});
|
|
30093
|
+
app2.get("/portal/projects/:id/backlog/:specId", async (req, reply) => {
|
|
30094
|
+
const { id, specId } = req.params;
|
|
30095
|
+
if (!await hasProjectAccess(req.user.id, id)) return reply.code(404).send(NOT_FOUND);
|
|
30096
|
+
const spec = (await liveSpecs({ project: id })).find((s2) => s2.id === specId);
|
|
30097
|
+
if (!spec) return reply.code(404).send(NOT_FOUND);
|
|
30098
|
+
return toPortalSpec(spec);
|
|
30099
|
+
});
|
|
30100
|
+
app2.get("/portal/projects/:id/tickets", async (req, reply) => {
|
|
30101
|
+
const { id } = req.params;
|
|
30102
|
+
if (!await hasProjectAccess(req.user.id, id)) return reply.code(404).send(NOT_FOUND);
|
|
30103
|
+
const { page, limit } = req.query;
|
|
30104
|
+
const rows = await prisma.ticket.findMany({ where: { projectId: id }, orderBy: { createdAt: "desc" } });
|
|
30105
|
+
const stages = await specStages(rows.map((t) => t.specId));
|
|
30106
|
+
return paginate(rows.map((t) => toPortalTicket(t, stages.get(t.specId ?? "") ?? null)), page, limit);
|
|
30107
|
+
});
|
|
30108
|
+
app2.get("/portal/projects/:id/tickets/:ticketId", async (req, reply) => {
|
|
30109
|
+
const { id, ticketId } = req.params;
|
|
30110
|
+
if (!await hasProjectAccess(req.user.id, id)) return reply.code(404).send(NOT_FOUND);
|
|
30111
|
+
const t = await prisma.ticket.findUnique({ where: { id: ticketId } });
|
|
30112
|
+
if (!t || t.projectId !== id) return reply.code(404).send(NOT_FOUND);
|
|
30113
|
+
const stages = await specStages([t.specId]);
|
|
30114
|
+
return toPortalTicketDetail(t, stages.get(t.specId ?? "") ?? null);
|
|
30115
|
+
});
|
|
30116
|
+
}
|
|
30117
|
+
|
|
30118
|
+
// src/routes/client-accounts.ts
|
|
29993
30119
|
init_src();
|
|
29994
30120
|
init_db();
|
|
29995
30121
|
|
|
@@ -30029,7 +30155,13 @@ async function lookupSession(token) {
|
|
|
30029
30155
|
});
|
|
30030
30156
|
return null;
|
|
30031
30157
|
}
|
|
30032
|
-
|
|
30158
|
+
if (s2.user.disabled) return null;
|
|
30159
|
+
return {
|
|
30160
|
+
id: s2.user.id,
|
|
30161
|
+
email: s2.user.email,
|
|
30162
|
+
role: s2.user.role,
|
|
30163
|
+
createdAt: s2.user.createdAt.toISOString()
|
|
30164
|
+
};
|
|
30033
30165
|
}
|
|
30034
30166
|
async function deleteSession(token) {
|
|
30035
30167
|
await prisma.session.deleteMany({ where: { id: sessionId(token) } });
|
|
@@ -30066,8 +30198,83 @@ function cookieOpts() {
|
|
|
30066
30198
|
};
|
|
30067
30199
|
}
|
|
30068
30200
|
|
|
30201
|
+
// src/routes/client-accounts.ts
|
|
30202
|
+
var view9 = (u) => ({
|
|
30203
|
+
id: u.id,
|
|
30204
|
+
email: u.email,
|
|
30205
|
+
disabled: u.disabled,
|
|
30206
|
+
createdAt: u.createdAt.toISOString(),
|
|
30207
|
+
projects: u.projectAccess.map((a) => a.projectId).sort()
|
|
30208
|
+
});
|
|
30209
|
+
var load = (id) => prisma.user.findFirst({
|
|
30210
|
+
where: { id, role: "client" },
|
|
30211
|
+
include: { projectAccess: { select: { projectId: true } } }
|
|
30212
|
+
});
|
|
30213
|
+
async function setAccess(userId, projects) {
|
|
30214
|
+
const ids = [...new Set(projects)];
|
|
30215
|
+
const known = await prisma.project.findMany({ where: { id: { in: ids } }, select: { id: true } });
|
|
30216
|
+
if (known.length !== ids.length) return null;
|
|
30217
|
+
await prisma.clientProjectAccess.deleteMany({ where: { userId, projectId: { notIn: ids } } });
|
|
30218
|
+
for (const projectId of ids)
|
|
30219
|
+
await prisma.clientProjectAccess.upsert({
|
|
30220
|
+
where: { userId_projectId: { userId, projectId } },
|
|
30221
|
+
update: {},
|
|
30222
|
+
create: { userId, projectId }
|
|
30223
|
+
});
|
|
30224
|
+
return ids;
|
|
30225
|
+
}
|
|
30226
|
+
async function client_accounts_default(app2) {
|
|
30227
|
+
app2.get("/client-accounts", async () => {
|
|
30228
|
+
const rows = await prisma.user.findMany({
|
|
30229
|
+
where: { role: "client" },
|
|
30230
|
+
orderBy: { createdAt: "asc" },
|
|
30231
|
+
include: { projectAccess: { select: { projectId: true } } }
|
|
30232
|
+
});
|
|
30233
|
+
return { items: rows.map(view9) };
|
|
30234
|
+
});
|
|
30235
|
+
app2.post("/client-accounts", async (req, reply) => {
|
|
30236
|
+
const p = zCreateClientAccount.safeParse(req.body);
|
|
30237
|
+
if (!p.success) return reply.code(400).send({ error: p.error.flatten() });
|
|
30238
|
+
if (await prisma.user.findUnique({ where: { email: p.data.email } }))
|
|
30239
|
+
return reply.code(409).send({ error: "email dipakai" });
|
|
30240
|
+
const user = await prisma.user.create({
|
|
30241
|
+
data: { email: p.data.email, passwordHash: await hashPassword(p.data.password), role: "client" }
|
|
30242
|
+
});
|
|
30243
|
+
if (await setAccess(user.id, p.data.projects) === null) {
|
|
30244
|
+
await prisma.user.delete({ where: { id: user.id } });
|
|
30245
|
+
return reply.code(400).send({ error: "ada project yang tidak dikenal" });
|
|
30246
|
+
}
|
|
30247
|
+
return reply.code(201).send(view9(await load(user.id)));
|
|
30248
|
+
});
|
|
30249
|
+
app2.patch("/client-accounts/:id", async (req, reply) => {
|
|
30250
|
+
const { id } = req.params;
|
|
30251
|
+
const p = zUpdateClientAccount.safeParse(req.body);
|
|
30252
|
+
if (!p.success) return reply.code(400).send({ error: p.error.flatten() });
|
|
30253
|
+
if (!await load(id)) return reply.code(404).send({ error: "not found" });
|
|
30254
|
+
if (p.data.projects && await setAccess(id, p.data.projects) === null)
|
|
30255
|
+
return reply.code(400).send({ error: "ada project yang tidak dikenal" });
|
|
30256
|
+
if (p.data.disabled !== void 0)
|
|
30257
|
+
await prisma.user.update({ where: { id }, data: { disabled: p.data.disabled } });
|
|
30258
|
+
if (p.data.password)
|
|
30259
|
+
await prisma.user.update({ where: { id }, data: { passwordHash: await hashPassword(p.data.password) } });
|
|
30260
|
+
if (p.data.disabled || p.data.password) await deleteUserSessions(id);
|
|
30261
|
+
return view9(await load(id));
|
|
30262
|
+
});
|
|
30263
|
+
app2.delete("/client-accounts/:id", async (req, reply) => {
|
|
30264
|
+
const { id } = req.params;
|
|
30265
|
+
if (!await load(id)) return reply.code(404).send({ error: "not found" });
|
|
30266
|
+
await prisma.user.delete({ where: { id } });
|
|
30267
|
+
return reply.code(204).send();
|
|
30268
|
+
});
|
|
30269
|
+
}
|
|
30270
|
+
|
|
30271
|
+
// src/app.ts
|
|
30272
|
+
var import_multipart = __toESM(require_multipart2(), 1);
|
|
30273
|
+
|
|
30069
30274
|
// src/routes/auth.ts
|
|
30070
|
-
|
|
30275
|
+
init_src();
|
|
30276
|
+
init_db();
|
|
30277
|
+
var view10 = (u) => ({ id: u.id, email: u.email, role: u.role, createdAt: u.createdAt.toISOString() });
|
|
30071
30278
|
async function issue(reply, userId) {
|
|
30072
30279
|
const token = await createSession2(userId);
|
|
30073
30280
|
reply.setCookie(COOKIE_NAME, token, cookieOpts());
|
|
@@ -30085,20 +30292,20 @@ async function auth_default(app2) {
|
|
|
30085
30292
|
data: { email: p.data.email, passwordHash: await hashPassword(p.data.password) }
|
|
30086
30293
|
});
|
|
30087
30294
|
await issue(reply, user.id);
|
|
30088
|
-
return { user:
|
|
30295
|
+
return { user: view10(user) };
|
|
30089
30296
|
});
|
|
30090
30297
|
app2.post("/auth/login", async (req, reply) => {
|
|
30091
30298
|
if (loginThrottle(req.ip).blocked) return reply.code(429).send({ error: "too many attempts" });
|
|
30092
30299
|
const p = zLogin.safeParse(req.body);
|
|
30093
30300
|
if (!p.success) return reply.code(400).send({ error: p.error.flatten() });
|
|
30094
30301
|
const user = await prisma.user.findUnique({ where: { email: p.data.email } });
|
|
30095
|
-
if (!user || !await verifyPassword(p.data.password, user.passwordHash)) {
|
|
30302
|
+
if (!user || user.disabled || !await verifyPassword(p.data.password, user.passwordHash)) {
|
|
30096
30303
|
noteLoginFail(req.ip);
|
|
30097
30304
|
return reply.code(401).send({ error: "email atau password salah" });
|
|
30098
30305
|
}
|
|
30099
30306
|
clearLoginFails(req.ip);
|
|
30100
30307
|
await issue(reply, user.id);
|
|
30101
|
-
return { user:
|
|
30308
|
+
return { user: view10(user) };
|
|
30102
30309
|
});
|
|
30103
30310
|
app2.post("/auth/logout", async (req, reply) => {
|
|
30104
30311
|
const token = req.cookies?.[COOKIE_NAME];
|
|
@@ -30106,19 +30313,24 @@ async function auth_default(app2) {
|
|
|
30106
30313
|
reply.clearCookie(COOKIE_NAME, { path: "/" });
|
|
30107
30314
|
return reply.code(204).send();
|
|
30108
30315
|
});
|
|
30109
|
-
app2.get("/auth/users", async () => (await prisma.user.findMany({ orderBy: { createdAt: "asc" } })).map(
|
|
30316
|
+
app2.get("/auth/users", async () => (await prisma.user.findMany({ orderBy: { createdAt: "asc" } })).map(view10));
|
|
30110
30317
|
app2.post("/auth/users", async (req, reply) => {
|
|
30111
30318
|
const p = zSignup.safeParse(req.body);
|
|
30112
30319
|
if (!p.success) return reply.code(400).send({ error: p.error.flatten() });
|
|
30113
30320
|
if (await prisma.user.findUnique({ where: { email: p.data.email } }))
|
|
30114
30321
|
return reply.code(409).send({ error: "email dipakai" });
|
|
30115
30322
|
const user = await prisma.user.create({
|
|
30116
|
-
|
|
30323
|
+
// SPEC-617 · pintu ini melahirkan OPERATOR. Akun klien punya pintunya sendiri
|
|
30324
|
+
// (/api/client-accounts) supaya "undang rekan" dan "beri akses klien" tak pernah tertukar.
|
|
30325
|
+
data: { email: p.data.email, passwordHash: await hashPassword(p.data.password), role: "admin" }
|
|
30117
30326
|
});
|
|
30118
|
-
return
|
|
30327
|
+
return view10(user);
|
|
30119
30328
|
});
|
|
30120
30329
|
app2.delete("/auth/users/:id", async (req, reply) => {
|
|
30121
|
-
|
|
30330
|
+
const target2 = await prisma.user.findUnique({ where: { id: req.params.id } });
|
|
30331
|
+
if (!target2) return reply.code(204).send();
|
|
30332
|
+
if (target2.role === "admin" && await prisma.user.count({ where: { role: "admin" } }) <= 1)
|
|
30333
|
+
return reply.code(400).send({ error: "tak bisa hapus admin terakhir" });
|
|
30122
30334
|
await prisma.user.deleteMany({ where: { id: req.params.id } });
|
|
30123
30335
|
return reply.code(204).send();
|
|
30124
30336
|
});
|
|
@@ -30135,7 +30347,7 @@ async function auth_default(app2) {
|
|
|
30135
30347
|
});
|
|
30136
30348
|
await deleteUserSessions(user.id);
|
|
30137
30349
|
await issue(reply, user.id);
|
|
30138
|
-
return { user:
|
|
30350
|
+
return { user: view10(user) };
|
|
30139
30351
|
});
|
|
30140
30352
|
}
|
|
30141
30353
|
|
|
@@ -30148,15 +30360,15 @@ async function agent_tokens_default(app2) {
|
|
|
30148
30360
|
app2.post("/agent-tokens", async (req, reply) => {
|
|
30149
30361
|
const p = zAgentTokenCreate.safeParse(req.body);
|
|
30150
30362
|
if (!p.success) return reply.code(400).send({ error: p.error.flatten() });
|
|
30151
|
-
const { view:
|
|
30152
|
-
return reply.code(201).send({ ...
|
|
30363
|
+
const { view: view11, token } = await issueAgentToken({ ...p.data, createdBy: req.user?.id });
|
|
30364
|
+
return reply.code(201).send({ ...view11, token });
|
|
30153
30365
|
});
|
|
30154
30366
|
app2.patch("/agent-tokens/:id", async (req, reply) => {
|
|
30155
30367
|
const p = zAgentTokenPatch.safeParse(req.body);
|
|
30156
30368
|
if (!p.success) return reply.code(400).send({ error: p.error.flatten() });
|
|
30157
30369
|
const { id } = req.params;
|
|
30158
|
-
const
|
|
30159
|
-
return
|
|
30370
|
+
const view11 = await patchAgentToken(id, p.data);
|
|
30371
|
+
return view11 ? reply.send(view11) : reply.code(404).send({ error: "not found" });
|
|
30160
30372
|
});
|
|
30161
30373
|
app2.delete("/agent-tokens/:id", async (req, reply) => {
|
|
30162
30374
|
const { id } = req.params;
|
|
@@ -30201,7 +30413,7 @@ function capabilityForRoute(method, path) {
|
|
|
30201
30413
|
const rw = (d) => `${d}:${read ? "read" : "write"}`;
|
|
30202
30414
|
const seg = path.replace(/^\/api\/?/, "").replace(/\/+$/, "").split("/").filter(Boolean);
|
|
30203
30415
|
const top = seg[0] ?? "";
|
|
30204
|
-
if (top === "auth" || top === "agent-tokens" || top === "device-tokens" || top === "sync") return "COOKIE_ONLY";
|
|
30416
|
+
if (top === "auth" || top === "agent-tokens" || top === "device-tokens" || top === "sync" || top === "portal" || top === "client-accounts") return "COOKIE_ONLY";
|
|
30205
30417
|
if (top === "limits" || top === "update" || top === "events" || top === "fs" || top === "health")
|
|
30206
30418
|
return read ? "GLOBAL_READ" : "COOKIE_ONLY";
|
|
30207
30419
|
if (top === "scheduler") return rw("settings");
|
|
@@ -30400,6 +30612,8 @@ function buildApp({ requireAuth = true, agentDocFile } = {}) {
|
|
|
30400
30612
|
if (user) req.user = user;
|
|
30401
30613
|
const path = req.url.split("?")[0] ?? req.url;
|
|
30402
30614
|
if (PUBLIC.has(`${req.method} ${path}`)) return;
|
|
30615
|
+
if (user?.role === "client" && !clientRouteAllowed(req.method, path))
|
|
30616
|
+
return reply.code(403).send({ error: "portal klien: baca-saja" });
|
|
30403
30617
|
if (path.startsWith("/api/sync") && path !== "/api/sync/now" && !path.startsWith("/api/sync/conflicts")) return;
|
|
30404
30618
|
if (path.startsWith("/api/help")) return;
|
|
30405
30619
|
if (user) return;
|
|
@@ -30455,6 +30669,8 @@ function buildApp({ requireAuth = true, agentDocFile } = {}) {
|
|
|
30455
30669
|
await api.register(telegramRoutes);
|
|
30456
30670
|
await api.register(webhooks_default);
|
|
30457
30671
|
await api.register(changelog_default);
|
|
30672
|
+
await api.register(portal_default);
|
|
30673
|
+
await api.register(client_accounts_default);
|
|
30458
30674
|
}, { prefix: "/api" });
|
|
30459
30675
|
if (process.env.NODE_ENV === "production") {
|
|
30460
30676
|
const dist = pickWebDir(dirname13(fileURLToPath4(import.meta.url)), process.env, existsSync13);
|
|
@@ -145,6 +145,10 @@ token selalu **403**, apa pun capability-nya, dan tak ada capability yang bisa m
|
|
|
145
145
|
mengalir keluar (ADR-0100)
|
|
146
146
|
- `/api/telegram/settings`, `/api/telegram/test`, `/api/telegram/credentials` — permukaan
|
|
147
147
|
**kredensial** (bot token & agent token), beda dari sisa `/api/telegram*` (ADR-0097)
|
|
148
|
+
- `/api/portal*` — portal klien: isinya ditentukan **akun yang login** (`ClientProjectAccess`), jadi
|
|
149
|
+
tak ada capability yang bisa berarti apa pun di sana. Backlog & tiket yang sama tersedia lewat
|
|
150
|
+
`/api/specs` dan `/api/tickets` dengan capability `backlog`/`support` (ADR-0110)
|
|
151
|
+
- `/api/client-accounts*` — membuat & mencabut akun klien beserta password awalnya (ADR-0110)
|
|
148
152
|
- `POST /api/update/apply` dan tulis lain di bawah prefix status (`/api/limits`, `/api/update`,
|
|
149
153
|
`/api/events`, `/api/fs`, `/api/health`) — **baca**-nya terbuka untuk token mana pun, **tulis**-nya
|
|
150
154
|
cookie-only
|
package/package.json
CHANGED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
-- SPEC-617 · ADR-0110 · peran user + pemetaan akun klien → project.
|
|
2
|
+
-- Additif & aman untuk DB yang sudah berjalan: user lama mendapat role 'admin' lewat DEFAULT,
|
|
3
|
+
-- jadi tak ada akses yang putus. (SQLite hanya melarang DEFAULT CURRENT_TIMESTAMP.)
|
|
4
|
+
ALTER TABLE "User" ADD COLUMN "role" TEXT NOT NULL DEFAULT 'admin';
|
|
5
|
+
ALTER TABLE "User" ADD COLUMN "disabled" BOOLEAN NOT NULL DEFAULT false;
|
|
6
|
+
|
|
7
|
+
CREATE TABLE "ClientProjectAccess" (
|
|
8
|
+
"id" TEXT NOT NULL PRIMARY KEY,
|
|
9
|
+
"userId" TEXT NOT NULL,
|
|
10
|
+
"projectId" TEXT NOT NULL,
|
|
11
|
+
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
12
|
+
CONSTRAINT "ClientProjectAccess_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
|
13
|
+
CONSTRAINT "ClientProjectAccess_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
|
14
|
+
);
|
|
15
|
+
CREATE UNIQUE INDEX "ClientProjectAccess_userId_projectId_key" ON "ClientProjectAccess"("userId", "projectId");
|
package/prisma/schema.prisma
CHANGED
|
@@ -29,6 +29,7 @@ model Project {
|
|
|
29
29
|
customAgents CustomAgent[] // SPEC-450 · ADR-0094 · custom agent milik project ini
|
|
30
30
|
githubIssues GithubIssue[] // SPEC-471 · ADR-0095 · cermin issue repo GitHub project ini
|
|
31
31
|
changelogs Changelog[] // SPEC-516 · ADR-0105 · changelog tersimpan project ini
|
|
32
|
+
clientAccess ClientProjectAccess[] // SPEC-617 · ADR-0110 · akun klien yang boleh melihat project ini
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
model Spec {
|
|
@@ -188,14 +189,37 @@ model Notification {
|
|
|
188
189
|
readAt DateTime?
|
|
189
190
|
}
|
|
190
191
|
|
|
191
|
-
// SPEC-169 · auth. passwordHash = scrypt "<saltHex>:<hashHex>".
|
|
192
|
+
// SPEC-169 · auth. passwordHash = scrypt "<saltHex>:<hashHex>".
|
|
193
|
+
// SPEC-617 · ADR-0110 · dua peran: `admin` (operator, akses penuh seperti sebelumnya) dan
|
|
194
|
+
// `client` (portal baca-saja ber-scope project). Default `"admin"` DISENGAJA: itulah yang
|
|
195
|
+
// membuat migrasi aman untuk instance yang sudah berjalan — setiap baris lama otomatis admin,
|
|
196
|
+
// tak ada akses yang putus dan tak ada backfill manual.
|
|
192
197
|
model User {
|
|
193
198
|
id String @id @default(cuid())
|
|
194
199
|
email String @unique
|
|
195
200
|
passwordHash String
|
|
201
|
+
role String @default("admin")
|
|
202
|
+
// SPEC-617 · nonaktif ditolak di login DAN di lookupSession; hanya menutup login berarti
|
|
203
|
+
// cookie yang sudah terbit tetap hidup sampai 7 hari (pencabutan yang tak mencabut).
|
|
204
|
+
disabled Boolean @default(false)
|
|
196
205
|
createdAt DateTime @default(now())
|
|
197
206
|
sessions Session[]
|
|
198
207
|
deviceTokens DeviceToken[]
|
|
208
|
+
projectAccess ClientProjectAccess[]
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// SPEC-617 · ADR-0110 · project yang boleh dilihat sebuah akun klien. Tabel join, bukan kolom
|
|
212
|
+
// Json: SQLite melarang scalar list, dan relasi memberi cascade delete gratis di kedua sisi.
|
|
213
|
+
// LOCAL-only — cermin User/Session/AgentToken, tak pernah masuk SYNCED (services/sync.ts).
|
|
214
|
+
model ClientProjectAccess {
|
|
215
|
+
id String @id @default(cuid())
|
|
216
|
+
userId String
|
|
217
|
+
projectId String
|
|
218
|
+
createdAt DateTime @default(now())
|
|
219
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
220
|
+
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
|
221
|
+
|
|
222
|
+
@@unique([userId, projectId])
|
|
199
223
|
}
|
|
200
224
|
|
|
201
225
|
// SPEC-169 · sesi opaque revocable. id = sha256(token); token asli hanya ada di cookie httpOnly.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
@import"https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:ital,wght@0,400;0,500;0,600;0,700;1,400&family=IBM+Plex+Serif:ital,wght@0,400;0,500;0,600;0,700;1,400&family=IBM+Plex+Mono:ital,wght@0,400;0,500;0,600;1,400&display=swap";:root{--ink-900: #17130c;--ink-800: #241d13;--ink-700: #3a3125;--ink-600: #574b3a;--ink-500: #6f6250;--ink-400: #90826c;--ink-300: #b3a794;--ink-200: #d6ccb9;--bone-000: #fffdf8;--bone-100: #faf6ec;--bone-200: #f2ebdc;--bone-300: #e8dfcc;--bone-400: #dccfb6;--brass-700: #7a5417;--brass-600: #97671f;--brass-500: #b8863b;--brass-400: #cf9d55;--brass-300: #e3c988;--brass-100: #f3e6c4;--brass-050: #faf2df;--wind-700: #2f5560;--wind-600: #3f6e7a;--wind-500: #517f8c;--wind-100: #dbe7ea;--wind-050: #eef4f5;--leaf-600: #4d6b30;--leaf-500: #5f8340;--leaf-100: #e6eed7;--amber-600: #b3771a;--amber-500: #cf8f28;--amber-100: #f7e9c9;--clay-600: #a23b2e;--clay-500: #bd4a3a;--clay-100: #f5dcd6;--term-bg: #1c1810;--term-fg: #e9e0cd;--term-dim: #8f8467;--term-line: #2c2618;--surface-page: var(--bone-100);--surface-panel: var(--bone-200);--surface-card: var(--bone-000);--surface-sunken: var(--bone-200);--surface-inverse: var(--ink-900);--surface-code: var(--term-bg);--text-strong: var(--ink-900);--text-body: var(--ink-700);--text-muted: var(--ink-500);--text-subtle: var(--ink-400);--text-on-brass: var(--ink-900);--text-on-dark: var(--term-fg);--text-link: var(--wind-600);--border-hair: var(--bone-300);--border-strong: var(--bone-400);--border-focus: var(--brass-500);--accent: var(--brass-500);--accent-hover: var(--brass-600);--accent-press: var(--brass-700);--accent-tint: var(--brass-100);--accent-on: var(--ink-900);--info: var(--wind-600);--info-tint: var(--wind-100);--status-ok: var(--leaf-600);--status-ok-tint: var(--leaf-100);--status-warn: var(--amber-600);--status-warn-tint: var(--amber-100);--status-err: var(--clay-600);--status-err-tint: var(--clay-100);--ring: 0 0 0 3px color-mix(in srgb, var(--brass-500) 45%, transparent)}:root{--font-serif: "IBM Plex Serif", Georgia, "Times New Roman", serif;--font-sans: "IBM Plex Sans", ui-sans-serif, system-ui, -apple-system, sans-serif;--font-mono: "IBM Plex Mono", ui-monospace, "SF Mono", Menlo, monospace;--font-display: var(--font-serif);--font-body: var(--font-sans);--font-ui: var(--font-sans);--font-code: var(--font-mono);--text-2xs: 11px;--text-xs: 12px;--text-sm: 13px;--text-md: 14px;--text-base:16px;--text-lg: 18px;--text-xl: 20px;--text-2xl: 24px;--text-3xl: 30px;--text-4xl: 38px;--text-5xl: 48px;--text-6xl: 62px;--weight-regular: 400;--weight-medium: 500;--weight-semibold: 600;--weight-bold: 700;--leading-tight: 1.12;--leading-snug: 1.3;--leading-normal: 1.5;--leading-relaxed:1.65;--tracking-tight: -.02em;--tracking-normal: 0;--tracking-wide: .02em;--tracking-caps: .14em}:root{--space-0: 0;--space-1: 4px;--space-2: 8px;--space-3: 12px;--space-4: 16px;--space-5: 20px;--space-6: 24px;--space-7: 32px;--space-8: 40px;--space-9: 48px;--space-10: 64px;--space-11: 80px;--space-12: 96px;--radius-xs: 3px;--radius-sm: 5px;--radius-md: 8px;--radius-lg: 12px;--radius-xl: 16px;--radius-pill: 999px;--border-thin: 1px;--border-med: 1.5px;--border-thick:2px;--content-max: 1200px;--sidebar-w: 248px;--topbar-h: 56px}:root{--shadow-xs: 0 1px 1px rgba(35, 27, 15, .05);--shadow-sm: 0 1px 2px rgba(35, 27, 15, .06), 0 1px 1px rgba(35, 27, 15, .04);--shadow-md: 0 2px 4px rgba(35, 27, 15, .07), 0 4px 12px rgba(35, 27, 15, .06);--shadow-lg: 0 8px 24px rgba(35, 27, 15, .1), 0 2px 6px rgba(35, 27, 15, .06);--shadow-xl: 0 20px 48px rgba(35, 27, 15, .16), 0 4px 12px rgba(35, 27, 15, .08);--shadow-inset: inset 0 1px 2px rgba(35, 27, 15, .08);--shadow-gild: 0 1px 0 rgba(255, 253, 248, .6) inset, 0 0 0 1px var(--brass-300);--elevation-flat: none;--elevation-raised: var(--shadow-sm);--elevation-float: var(--shadow-md);--elevation-overlay:var(--shadow-lg);--elevation-modal: var(--shadow-xl);--ease-out: cubic-bezier(.22, .61, .36, 1);--ease-inout: cubic-bezier(.65, 0, .35, 1);--dur-fast: .12s;--dur-base: .18s;--dur-slow: .28s;--transition-base: all var(--dur-base) var(--ease-out);--transition-fast: all var(--dur-fast) var(--ease-out)}*,*:before,*:after{box-sizing:border-box}body{margin:0;background:var(--surface-page);color:var(--text-body);font-family:var(--font-body);font-size:var(--text-base);line-height:var(--leading-normal);-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility}h1,h2,h3,h4{font-family:var(--font-display);color:var(--text-strong);line-height:var(--leading-tight);letter-spacing:var(--tracking-tight);font-weight:var(--weight-semibold);margin:0 0 var(--space-3)}p{margin:0 0 var(--space-3)}a{color:var(--text-link);text-decoration:none;border-bottom:1px solid color-mix(in srgb,var(--text-link) 35%,transparent);transition:var(--transition-fast)}a:hover{color:var(--wind-700);border-bottom-color:var(--wind-700)}code,kbd,samp,pre{font-family:var(--font-code);font-size:.92em}.hn-eyebrow{font-family:var(--font-mono);font-size:var(--text-2xs);font-weight:var(--weight-medium);letter-spacing:var(--tracking-caps);text-transform:uppercase;color:var(--text-muted)}::selection{background:var(--brass-100);color:var(--ink-900)}html,body{margin:0;height:100%;background:var(--surface-page)}#root{height:100vh;overflow:hidden}button{--accent-on: #fff}a{color:var(--text-link);text-decoration:none}a:hover{color:var(--wind-700);text-decoration:underline}.hn-md{color:var(--text-body);font-family:var(--font-sans);font-size:15px;line-height:1.72;overflow-wrap:anywhere}.hn-md>:first-child{margin-top:8px}.hn-md h1,.hn-md h2,.hn-md h3,.hn-md h4{font-family:var(--font-display);font-weight:600;letter-spacing:-.02em;color:var(--ink-900);line-height:1.2;margin:1.6em 0 .5em}.hn-md h1{font-size:30px;margin-top:.4em;padding-bottom:.34em;border-bottom:1px solid var(--border-hair)}.hn-md h2{font-size:22px}.hn-md h3{font-size:18px}.hn-md h4{font-size:15px}.hn-md p{margin:.7em 0}.hn-md ul,.hn-md ol{margin:.6em 0;padding-left:1.5em}.hn-md li{margin:.28em 0}.hn-md li::marker{color:var(--brass-500)}.hn-md ul.contains-task-list,.hn-md li.task-list-item{list-style:none}.hn-md li.task-list-item{padding-left:0}.hn-md input[type=checkbox]{accent-color:var(--brass-500);margin-right:8px;transform:translateY(1px)}.hn-md strong{font-weight:600;color:var(--ink-900)}.hn-md em{color:var(--text-muted)}.hn-md a{color:var(--text-link)}.hn-md hr{border:none;border-top:1px solid var(--border-hair);margin:1.6em 0}.hn-md code{font-family:var(--font-mono);font-size:.86em;background:var(--bone-200);color:var(--ink-800);padding:1.5px 5px;border-radius:var(--radius-xs);border:1px solid var(--border-hair)}.hn-md pre{background:var(--surface-code);border:1px solid var(--term-line);border-radius:var(--radius-lg);padding:14px 16px;overflow:auto;margin:1em 0;white-space:pre-wrap;overflow-wrap:anywhere}.hn-md pre code{background:transparent;border:none;padding:0;color:var(--term-fg);font-size:12.5px;line-height:1.7}.hn-md blockquote{margin:1em 0;padding:4px 16px;border-left:3px solid var(--brass-500);background:var(--brass-050);color:var(--text-body);border-radius:0 var(--radius-sm) var(--radius-sm) 0}.hn-md blockquote p{margin:.4em 0}.hn-md table{border-collapse:collapse;width:100%;margin:1em 0;font-size:14px;table-layout:fixed}.hn-md th,.hn-md td{overflow-wrap:anywhere}.hn-md th{font-family:var(--font-mono);font-size:11px;text-transform:uppercase;letter-spacing:.08em;color:var(--text-muted);text-align:left;padding:8px 12px;background:var(--bone-100);border:1px solid var(--border-hair)}.hn-md td{padding:8px 12px;border:1px solid var(--border-hair);color:var(--text-body)}.hn-md td code{font-size:.82em}.hn-md img{max-width:100%;border-radius:var(--radius-sm)}@keyframes hn-fade-in{0%{opacity:0}to{opacity:1}}@keyframes hn-modal-in{0%{opacity:0;transform:translateY(8px) scale(.99)}to{opacity:1;transform:none}}@keyframes hn-toast-in{0%{opacity:0;transform:translate(-50%,10px)}to{opacity:1;transform:translate(-50%)}}/**
|
|
1
|
+
@import"https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:ital,wght@0,400;0,500;0,600;0,700;1,400&family=IBM+Plex+Serif:ital,wght@0,400;0,500;0,600;0,700;1,400&family=IBM+Plex+Mono:ital,wght@0,400;0,500;0,600;1,400&display=swap";:root{--ink-900: #17130c;--ink-800: #241d13;--ink-700: #3a3125;--ink-600: #574b3a;--ink-500: #6f6250;--ink-400: #90826c;--ink-300: #b3a794;--ink-200: #d6ccb9;--bone-000: #fffdf8;--bone-100: #faf6ec;--bone-200: #f2ebdc;--bone-300: #e8dfcc;--bone-400: #dccfb6;--brass-700: #7a5417;--brass-600: #97671f;--brass-500: #b8863b;--brass-400: #cf9d55;--brass-300: #e3c988;--brass-100: #f3e6c4;--brass-050: #faf2df;--wind-700: #2f5560;--wind-600: #3f6e7a;--wind-500: #517f8c;--wind-100: #dbe7ea;--wind-050: #eef4f5;--leaf-600: #4d6b30;--leaf-500: #5f8340;--leaf-100: #e6eed7;--amber-600: #b3771a;--amber-500: #cf8f28;--amber-100: #f7e9c9;--clay-600: #a23b2e;--clay-500: #bd4a3a;--clay-100: #f5dcd6;--term-bg: #1c1810;--term-fg: #e9e0cd;--term-dim: #8f8467;--term-line: #2c2618;--surface-page: var(--bone-100);--surface-panel: var(--bone-200);--surface-card: var(--bone-000);--surface-sunken: var(--bone-200);--surface-inverse: var(--ink-900);--surface-code: var(--term-bg);--text-strong: var(--ink-900);--text-body: var(--ink-700);--text-muted: var(--ink-500);--text-subtle: var(--ink-400);--text-on-brass: var(--ink-900);--text-on-dark: var(--term-fg);--text-link: var(--wind-600);--border-hair: var(--bone-300);--border-strong: var(--bone-400);--border-focus: var(--brass-500);--accent: var(--brass-500);--accent-hover: var(--brass-600);--accent-press: var(--brass-700);--accent-tint: var(--brass-100);--accent-on: var(--ink-900);--info: var(--wind-600);--info-tint: var(--wind-100);--status-ok: var(--leaf-600);--status-ok-tint: var(--leaf-100);--status-warn: var(--amber-600);--status-warn-tint: var(--amber-100);--status-err: var(--clay-600);--status-err-tint: var(--clay-100);--ring: 0 0 0 3px color-mix(in srgb, var(--brass-500) 45%, transparent)}:root{--font-serif: "IBM Plex Serif", Georgia, "Times New Roman", serif;--font-sans: "IBM Plex Sans", ui-sans-serif, system-ui, -apple-system, sans-serif;--font-mono: "IBM Plex Mono", ui-monospace, "SF Mono", Menlo, monospace;--font-display: var(--font-serif);--font-body: var(--font-sans);--font-ui: var(--font-sans);--font-code: var(--font-mono);--text-2xs: 11px;--text-xs: 12px;--text-sm: 13px;--text-md: 14px;--text-base:16px;--text-lg: 18px;--text-xl: 20px;--text-2xl: 24px;--text-3xl: 30px;--text-4xl: 38px;--text-5xl: 48px;--text-6xl: 62px;--weight-regular: 400;--weight-medium: 500;--weight-semibold: 600;--weight-bold: 700;--leading-tight: 1.12;--leading-snug: 1.3;--leading-normal: 1.5;--leading-relaxed:1.65;--tracking-tight: -.02em;--tracking-normal: 0;--tracking-wide: .02em;--tracking-caps: .14em}:root{--space-0: 0;--space-1: 4px;--space-2: 8px;--space-3: 12px;--space-4: 16px;--space-5: 20px;--space-6: 24px;--space-7: 32px;--space-8: 40px;--space-9: 48px;--space-10: 64px;--space-11: 80px;--space-12: 96px;--radius-xs: 3px;--radius-sm: 5px;--radius-md: 8px;--radius-lg: 12px;--radius-xl: 16px;--radius-pill: 999px;--border-thin: 1px;--border-med: 1.5px;--border-thick:2px;--content-max: 1200px;--sidebar-w: 248px;--topbar-h: 56px}:root{--shadow-xs: 0 1px 1px rgba(35, 27, 15, .05);--shadow-sm: 0 1px 2px rgba(35, 27, 15, .06), 0 1px 1px rgba(35, 27, 15, .04);--shadow-md: 0 2px 4px rgba(35, 27, 15, .07), 0 4px 12px rgba(35, 27, 15, .06);--shadow-lg: 0 8px 24px rgba(35, 27, 15, .1), 0 2px 6px rgba(35, 27, 15, .06);--shadow-xl: 0 20px 48px rgba(35, 27, 15, .16), 0 4px 12px rgba(35, 27, 15, .08);--shadow-inset: inset 0 1px 2px rgba(35, 27, 15, .08);--shadow-gild: 0 1px 0 rgba(255, 253, 248, .6) inset, 0 0 0 1px var(--brass-300);--elevation-flat: none;--elevation-raised: var(--shadow-sm);--elevation-float: var(--shadow-md);--elevation-overlay:var(--shadow-lg);--elevation-modal: var(--shadow-xl);--ease-out: cubic-bezier(.22, .61, .36, 1);--ease-inout: cubic-bezier(.65, 0, .35, 1);--dur-fast: .12s;--dur-base: .18s;--dur-slow: .28s;--transition-base: all var(--dur-base) var(--ease-out);--transition-fast: all var(--dur-fast) var(--ease-out)}*,*:before,*:after{box-sizing:border-box}body{margin:0;background:var(--surface-page);color:var(--text-body);font-family:var(--font-body);font-size:var(--text-base);line-height:var(--leading-normal);-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility}h1,h2,h3,h4{font-family:var(--font-display);color:var(--text-strong);line-height:var(--leading-tight);letter-spacing:var(--tracking-tight);font-weight:var(--weight-semibold);margin:0 0 var(--space-3)}p{margin:0 0 var(--space-3)}a{color:var(--text-link);text-decoration:none;border-bottom:1px solid color-mix(in srgb,var(--text-link) 35%,transparent);transition:var(--transition-fast)}a:hover{color:var(--wind-700);border-bottom-color:var(--wind-700)}code,kbd,samp,pre{font-family:var(--font-code);font-size:.92em}.hn-eyebrow{font-family:var(--font-mono);font-size:var(--text-2xs);font-weight:var(--weight-medium);letter-spacing:var(--tracking-caps);text-transform:uppercase;color:var(--text-muted)}::selection{background:var(--brass-100);color:var(--ink-900)}html,body{margin:0;height:100%;background:var(--surface-page)}#root{height:100vh;overflow:hidden}button{--accent-on: #fff}a{color:var(--text-link);text-decoration:none}a:hover{color:var(--wind-700);text-decoration:underline}.hn-md{color:var(--text-body);font-family:var(--font-sans);font-size:15px;line-height:1.72;overflow-wrap:anywhere}.hn-md>:first-child{margin-top:8px}.hn-md h1,.hn-md h2,.hn-md h3,.hn-md h4{font-family:var(--font-display);font-weight:600;letter-spacing:-.02em;color:var(--ink-900);line-height:1.2;margin:1.6em 0 .5em}.hn-md h1{font-size:30px;margin-top:.4em;padding-bottom:.34em;border-bottom:1px solid var(--border-hair)}.hn-md h2{font-size:22px}.hn-md h3{font-size:18px}.hn-md h4{font-size:15px}.hn-md p{margin:.7em 0}.hn-md ul,.hn-md ol{margin:.6em 0;padding-left:1.5em}.hn-md li{margin:.28em 0}.hn-md li::marker{color:var(--brass-500)}.hn-md ul.contains-task-list,.hn-md li.task-list-item{list-style:none}.hn-md li.task-list-item{padding-left:0}.hn-md input[type=checkbox]{accent-color:var(--brass-500);margin-right:8px;transform:translateY(1px)}.hn-md strong{font-weight:600;color:var(--ink-900)}.hn-md em{color:var(--text-muted)}.hn-md a{color:var(--text-link)}.hn-md hr{border:none;border-top:1px solid var(--border-hair);margin:1.6em 0}.hn-md code{font-family:var(--font-mono);font-size:.86em;background:var(--bone-200);color:var(--ink-800);padding:1.5px 5px;border-radius:var(--radius-xs);border:1px solid var(--border-hair)}.hn-md pre{background:var(--surface-code);border:1px solid var(--term-line);border-radius:var(--radius-lg);padding:14px 16px;overflow:auto;margin:1em 0;white-space:pre-wrap;overflow-wrap:anywhere}.hn-md pre code{background:transparent;border:none;padding:0;color:var(--term-fg);font-size:12.5px;line-height:1.7}.hn-md blockquote{margin:1em 0;padding:4px 16px;border-left:3px solid var(--brass-500);background:var(--brass-050);color:var(--text-body);border-radius:0 var(--radius-sm) var(--radius-sm) 0}.hn-md blockquote p{margin:.4em 0}.hn-md table{border-collapse:collapse;width:100%;margin:1em 0;font-size:14px;table-layout:fixed}.hn-md th,.hn-md td{overflow-wrap:anywhere}.hn-md th{font-family:var(--font-mono);font-size:11px;text-transform:uppercase;letter-spacing:.08em;color:var(--text-muted);text-align:left;padding:8px 12px;background:var(--bone-100);border:1px solid var(--border-hair)}.hn-md td{padding:8px 12px;border:1px solid var(--border-hair);color:var(--text-body)}.hn-md td code{font-size:.82em}.hn-md img{max-width:100%;border-radius:var(--radius-sm)}@keyframes hn-fade-in{0%{opacity:0}to{opacity:1}}@keyframes hn-modal-in{0%{opacity:0;transform:translateY(8px) scale(.99)}to{opacity:1;transform:none}}@keyframes hn-toast-in{0%{opacity:0;transform:translate(-50%,10px)}to{opacity:1;transform:translate(-50%)}}@keyframes hn-pet-breathe{0%{transform:translateY(0) scale(1)}to{transform:translateY(-3px) scale(1.025)}}/**
|
|
2
2
|
* Copyright (c) 2014 The xterm.js authors. All rights reserved.
|
|
3
3
|
* Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
|
|
4
4
|
* https://github.com/chjj/term.js
|