@prismshadow/penguin-server 0.2.15 → 0.2.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/api/types.d.ts +318 -86
- package/dist/api/types.js +9 -3
- package/dist/auth-token.d.ts +1 -1
- package/dist/auth-token.js +2 -2
- package/dist/{chunk-HMTT2PX4.js → chunk-2LZDP26O.js} +8 -5
- package/dist/chunk-2LZDP26O.js.map +1 -0
- package/dist/{chunk-LPPVGHYD.js → chunk-BL4U7GYY.js} +2 -2
- package/dist/chunk-BL4U7GYY.js.map +1 -0
- package/dist/{chunk-5Z4UCHDB.js → chunk-D3JNA6ZI.js} +69 -6
- package/dist/chunk-D3JNA6ZI.js.map +1 -0
- package/dist/{chunk-TOOTEEM4.js → chunk-G7GY7PJU.js} +1 -1
- package/dist/chunk-G7GY7PJU.js.map +1 -0
- package/dist/chunk-GGIINEEK.js +3401 -0
- package/dist/chunk-GGIINEEK.js.map +1 -0
- package/dist/chunk-HDBZRVA5.js +29 -0
- package/dist/chunk-HDBZRVA5.js.map +1 -0
- package/dist/{chunk-NVWAYYRX.js → chunk-HPOHMZCH.js} +686 -531
- package/dist/chunk-HPOHMZCH.js.map +1 -0
- package/dist/{chunk-QFV3ERQO.js → chunk-OQZDL3TR.js} +2 -2
- package/dist/hmr/manifest.js +1 -1
- package/dist/index.js +9351 -5551
- package/dist/index.js.map +1 -1
- package/dist/machine-status.js +2 -2
- package/dist/model-catalog.d.ts +610 -0
- package/dist/model-catalog.js +108 -0
- package/dist/model-catalog.js.map +1 -0
- package/dist/plugin/index.d.ts +141 -78
- package/dist/reset-admin-password.js +3 -4
- package/dist/reset-admin-password.js.map +1 -1
- package/dist/version-report.js +2 -2
- package/package.json +15 -14
- package/web-dist/assets/{abnfDiagram-VCTEODGH-CKWh4hoA.js → abnfDiagram-VCTEODGH-DMzkb4l4.js} +1 -1
- package/web-dist/assets/{addon-clipboard-DxLqat7q.js → addon-clipboard-BtD2BVSi.js} +1 -1
- package/web-dist/assets/{addon-fit-B8oPGPkW.js → addon-fit-Dr6HWsi_.js} +1 -1
- package/web-dist/assets/{addon-web-links-C0W5TcK0.js → addon-web-links-VEE3YLzQ.js} +1 -1
- package/web-dist/assets/{arc-Dp9EkaAH.js → arc-CvMPOU_B.js} +1 -1
- package/web-dist/assets/{architectureDiagram-5GKGNRK7-BQ3LUcle.js → architectureDiagram-5GKGNRK7-Bk65mIBz.js} +1 -1
- package/web-dist/assets/{blockDiagram-I7D4REHJ-CZzyU0Jj.js → blockDiagram-I7D4REHJ-D54-XSNs.js} +1 -1
- package/web-dist/assets/{c4Diagram-7LVT6UL2-Qps3jeAu.js → c4Diagram-7LVT6UL2-Dm954LSt.js} +1 -1
- package/web-dist/assets/channel-CVHI75kz.js +1 -0
- package/web-dist/assets/{chunk-2Q5K7J3B-CQMntzuX.js → chunk-2Q5K7J3B-D_LJ_7Cn.js} +1 -1
- package/web-dist/assets/{chunk-5VM5RSS4-2H2ZJAte.js → chunk-5VM5RSS4-B8RI4tl3.js} +1 -1
- package/web-dist/assets/{chunk-F27PBJKO-B30xCNBv.js → chunk-F27PBJKO-aVSYH9JV.js} +1 -1
- package/web-dist/assets/{chunk-IMKFNOWR-Cyfx4zDs.js → chunk-IMKFNOWR-DnkdWeq2.js} +1 -1
- package/web-dist/assets/{chunk-JWPE2WC7-DADUytTh.js → chunk-JWPE2WC7-C2pJzKuW.js} +1 -1
- package/web-dist/assets/{chunk-POPQ4Y6H-9bjsAxLS.js → chunk-POPQ4Y6H-Bx7Iikzx.js} +1 -1
- package/web-dist/assets/{chunk-SVP7TREG-B9xO1VpN.js → chunk-SVP7TREG-qJC2SMqr.js} +1 -1
- package/web-dist/assets/{chunk-TICWLB2K-B6oDkrep.js → chunk-TICWLB2K-Crl_BgKI.js} +1 -1
- package/web-dist/assets/{chunk-XXDRQBXY-BbU8DR4B.js → chunk-XXDRQBXY-BFFemuGm.js} +1 -1
- package/web-dist/assets/classDiagram-ZZMXUADV-CBdzAPFu.js +1 -0
- package/web-dist/assets/classDiagram-v2-VYDZK3BY-CBdzAPFu.js +1 -0
- package/web-dist/assets/{cose-bilkent-JH36ORCC-pkbuyec-.js → cose-bilkent-JH36ORCC-Bmh2Kx7h.js} +1 -1
- package/web-dist/assets/{cynefin-OW5HDTMX-DU9YQQnc.js → cynefin-OW5HDTMX-BveNWnUV.js} +1 -1
- package/web-dist/assets/{cynefinDiagram-5FMLGOSQ-3a3XuhQq.js → cynefinDiagram-5FMLGOSQ-85itazbJ.js} +1 -1
- package/web-dist/assets/{dagre-GXQ25YYZ-BfZ9C5CZ.js → dagre-GXQ25YYZ-DIZT-KY_.js} +1 -1
- package/web-dist/assets/{diagram-S7CK7UJ4-BoGhVBMn.js → diagram-S7CK7UJ4-Dum-g63f.js} +1 -1
- package/web-dist/assets/{diagram-UQ7AKVKN-zE67quty.js → diagram-UQ7AKVKN-DallMKJl.js} +1 -1
- package/web-dist/assets/{diagram-VSXAHHWV-NihoXeXm.js → diagram-VSXAHHWV-DWELqxfN.js} +1 -1
- package/web-dist/assets/{diagram-VX7I27RA-1627np0S.js → diagram-VX7I27RA-BNs0gB9E.js} +1 -1
- package/web-dist/assets/{diagram-Z3DM3KII-Ddg_bUan.js → diagram-Z3DM3KII-C9mF9xAd.js} +1 -1
- package/web-dist/assets/{ebnfDiagram-PWID7BFC-CmPUOM4j.js → ebnfDiagram-PWID7BFC-D_Diy9HH.js} +1 -1
- package/web-dist/assets/{erDiagram-RLTQ6QDP-DiTnEK72.js → erDiagram-RLTQ6QDP-4QTS5VIh.js} +1 -1
- package/web-dist/assets/{flowDiagram-HODETNUW-mGQ38tRM.js → flowDiagram-HODETNUW-CliTRrtV.js} +1 -1
- package/web-dist/assets/{ganttDiagram-EL5Y4UJY-DU7MlzpW.js → ganttDiagram-EL5Y4UJY-DCRd-Ss4.js} +1 -1
- package/web-dist/assets/{gitGraphDiagram-WWUBYQGX-CRE_3vjl.js → gitGraphDiagram-WWUBYQGX-CTz13GjS.js} +1 -1
- package/web-dist/assets/highlighter-BScH1Ud3.js +2 -0
- package/web-dist/assets/{highlighter-core-B5CvXcEK.js → highlighter-core-DKz8Z6-I.js} +1 -1
- package/web-dist/assets/{highlighter.worker-D6D9fPma.js → highlighter.worker-CKF6ok1g.js} +1 -1
- package/web-dist/assets/index-BUvJ5Ctt.js +1299 -0
- package/web-dist/assets/index-DRj7nvPY.css +1 -0
- package/web-dist/assets/{infoDiagram-27XIBGKW-Cdo6Ar-a.js → infoDiagram-27XIBGKW-DNfTEPAV.js} +1 -1
- package/web-dist/assets/{ishikawaDiagram-5VMMS53U-C27pgect.js → ishikawaDiagram-5VMMS53U-B7aCjjG1.js} +1 -1
- package/web-dist/assets/{journeyDiagram-3NMN7TZE-CVxtKpLG.js → journeyDiagram-3NMN7TZE-COmDALVr.js} +1 -1
- package/web-dist/assets/{kanban-definition-UXKFOSKX-DWA31Da7.js → kanban-definition-UXKFOSKX-BzUfPINh.js} +1 -1
- package/web-dist/assets/{layout-DWKCU9PO.js → layout-BwzuPU4M.js} +1 -1
- package/web-dist/assets/{linear-BwCthj_k.js → linear-WPZTYjz3.js} +1 -1
- package/web-dist/assets/mermaid.core-zKzlwpkw.js +310 -0
- package/web-dist/assets/{mindmap-definition-YA3MSWOX-BLEDhbn1.js → mindmap-definition-YA3MSWOX-DCRg7eAx.js} +1 -1
- package/web-dist/assets/{pegDiagram-XKGWAZYB-CLEM1SDU.js → pegDiagram-XKGWAZYB-DyphzDLU.js} +1 -1
- package/web-dist/assets/{pieDiagram-E7YTZNPT-B__g6eF0.js → pieDiagram-E7YTZNPT-BI5DueGA.js} +1 -1
- package/web-dist/assets/powershell-DXrKVo3G.js +1 -0
- package/web-dist/assets/powershell-Dpen1YoG.js +1 -0
- package/web-dist/assets/{quadrantDiagram-AXDQQJYC-C7VQ2LMY.js → quadrantDiagram-AXDQQJYC-GZxSMY2Z.js} +1 -1
- package/web-dist/assets/{railroadDiagram-O6MQD6OU-eWNnzjwS.js → railroadDiagram-O6MQD6OU-DEFCigBf.js} +1 -1
- package/web-dist/assets/{requirementDiagram-BXWQKSXE-Bi5qeD4b.js → requirementDiagram-BXWQKSXE-Br0thc4j.js} +1 -1
- package/web-dist/assets/{sankeyDiagram-P5KCCOFB-DG1VOpXX.js → sankeyDiagram-P5KCCOFB-DYf0pY67.js} +1 -1
- package/web-dist/assets/{sequenceDiagram-WJ2MYXX4-Fhkw73Th.js → sequenceDiagram-WJ2MYXX4-1EFMboX4.js} +1 -1
- package/web-dist/assets/{sizeCapture-INFHLROL-D9E_17SF.js → sizeCapture-INFHLROL-BuOFv-5q.js} +1 -1
- package/web-dist/assets/{stateDiagram-D77RDMKH-Cs5Y0xhW.js → stateDiagram-D77RDMKH-Bg3H1tjX.js} +1 -1
- package/web-dist/assets/stateDiagram-v2-MP3YSRHH-DfaDaPAd.js +1 -0
- package/web-dist/assets/{swimlanes-42K2YHIH-t-EocvKZ.js → swimlanes-42K2YHIH-BuOa6Osb.js} +1 -1
- package/web-dist/assets/swimlanesDiagram-VR7AAH4N-BopAC7b8.js +8 -0
- package/web-dist/assets/{timeline-definition-24CTP7MA-CozGCxuA.js → timeline-definition-24CTP7MA-BDekfdDT.js} +1 -1
- package/web-dist/assets/{vennDiagram-4TSXK5OY-ewU8ycSa.js → vennDiagram-4TSXK5OY-DnC09UI4.js} +1 -1
- package/web-dist/assets/{wardleyDiagram-VM6X3IG4-DuAhDu-0.js → wardleyDiagram-VM6X3IG4-BXKvxuZQ.js} +1 -1
- package/web-dist/assets/{xterm-v5e56GC9.js → xterm-oY4DYg5s.js} +1 -1
- package/web-dist/assets/{xychartDiagram-S5SC5T6Z-BxGuZO23.js → xychartDiagram-S5SC5T6Z-DdDC1VLb.js} +1 -1
- package/web-dist/index.html +12 -6
- package/web-dist/{workflow-ui.css → workbench-ui.css} +72 -67
- package/dist/benchmark-package.d.ts +0 -39
- package/dist/benchmark-package.js +0 -97
- package/dist/benchmark-package.js.map +0 -1
- package/dist/chunk-54E64LWB.js +0 -63
- package/dist/chunk-54E64LWB.js.map +0 -1
- package/dist/chunk-5Z4UCHDB.js.map +0 -1
- package/dist/chunk-HMTT2PX4.js.map +0 -1
- package/dist/chunk-JKJXQTHH.js +0 -19
- package/dist/chunk-JKJXQTHH.js.map +0 -1
- package/dist/chunk-LPPVGHYD.js.map +0 -1
- package/dist/chunk-NVWAYYRX.js.map +0 -1
- package/dist/chunk-Q4ON2L6E.js +0 -52
- package/dist/chunk-Q4ON2L6E.js.map +0 -1
- package/dist/chunk-TOOTEEM4.js.map +0 -1
- package/web-dist/assets/channel--PblgLlC.js +0 -1
- package/web-dist/assets/classDiagram-ZZMXUADV-mn8tbuqa.js +0 -1
- package/web-dist/assets/classDiagram-v2-VYDZK3BY-mn8tbuqa.js +0 -1
- package/web-dist/assets/highlighter-CzfXQ6yd.js +0 -2
- package/web-dist/assets/index-BdG4qRyI.js +0 -1275
- package/web-dist/assets/index-W-xYFoIm.css +0 -1
- package/web-dist/assets/mermaid.core-BcTrmPOv.js +0 -310
- package/web-dist/assets/stateDiagram-v2-MP3YSRHH-CEM51fAf.js +0 -1
- package/web-dist/assets/swimlanesDiagram-VR7AAH4N-BrR66vrK.js +0 -8
- /package/dist/{chunk-QFV3ERQO.js.map → chunk-OQZDL3TR.js.map} +0 -0
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Base stylesheet for a
|
|
2
|
+
* Base stylesheet for a workbench's own page (features/workbenches/workbench-tabs.tsx injects it
|
|
3
3
|
* into the frame, and a page may link it itself for standalone viewing).
|
|
4
4
|
*
|
|
5
|
-
* A
|
|
5
|
+
* A workbench page is a separate document: Tailwind never sees it, and the app's `html.dark`
|
|
6
6
|
* class and accent variables stop at the frame boundary. So the app copies its RESOLVED
|
|
7
7
|
* tokens — the gray scale, the accent pair, the font stack, the root font size — onto this
|
|
8
|
-
* document's root (lib/
|
|
8
|
+
* document's root (lib/workbench-theme.ts), and everything here is expressed in those
|
|
9
9
|
* variables. The literal fallbacks are only for a page opened outside the app; the palette
|
|
10
10
|
* has exactly one source of truth, styles.css.
|
|
11
11
|
*
|
|
@@ -16,16 +16,16 @@
|
|
|
16
16
|
|
|
17
17
|
:root {
|
|
18
18
|
color-scheme: light;
|
|
19
|
-
--
|
|
20
|
-
--
|
|
21
|
-
--
|
|
22
|
-
--
|
|
23
|
-
--
|
|
24
|
-
--
|
|
25
|
-
--
|
|
26
|
-
--
|
|
27
|
-
--
|
|
28
|
-
--
|
|
19
|
+
--wb-bg: #ffffff;
|
|
20
|
+
--wb-surface: var(--color-gray-50, #f9fafb);
|
|
21
|
+
--wb-fg: var(--color-gray-900, #111827);
|
|
22
|
+
--wb-muted: var(--color-gray-500, #6b7280);
|
|
23
|
+
--wb-border: var(--color-gray-200, #e5e7eb);
|
|
24
|
+
--wb-hover: var(--color-gray-100, #f3f4f6);
|
|
25
|
+
--wb-accent: var(--ui-accent, #111827);
|
|
26
|
+
--wb-accent-fg: var(--ui-accent-fg, #ffffff);
|
|
27
|
+
--wb-radius: 6px;
|
|
28
|
+
--wb-font: var(
|
|
29
29
|
--font-app-sans,
|
|
30
30
|
ui-sans-serif,
|
|
31
31
|
system-ui,
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"Microsoft YaHei",
|
|
37
37
|
sans-serif
|
|
38
38
|
);
|
|
39
|
-
--
|
|
39
|
+
--wb-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
/* The app stamps `dark` or `light` on the root; `prefers-color-scheme` only decides for a
|
|
@@ -44,31 +44,31 @@
|
|
|
44
44
|
@media (prefers-color-scheme: dark) {
|
|
45
45
|
:root:not(.light) {
|
|
46
46
|
color-scheme: dark;
|
|
47
|
-
--
|
|
48
|
-
--
|
|
49
|
-
--
|
|
50
|
-
--
|
|
51
|
-
--
|
|
52
|
-
--
|
|
47
|
+
--wb-bg: var(--color-gray-950, #121212);
|
|
48
|
+
--wb-surface: var(--color-gray-900, #1a1a1a);
|
|
49
|
+
--wb-fg: var(--color-gray-100, #f5f5f5);
|
|
50
|
+
--wb-muted: var(--color-gray-400, #a1a1a1);
|
|
51
|
+
--wb-border: var(--color-gray-700, #333333);
|
|
52
|
+
--wb-hover: var(--color-gray-800, #262626);
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
:root.dark {
|
|
57
57
|
color-scheme: dark;
|
|
58
|
-
--
|
|
59
|
-
--
|
|
60
|
-
--
|
|
61
|
-
--
|
|
62
|
-
--
|
|
63
|
-
--
|
|
58
|
+
--wb-bg: var(--color-gray-950, #121212);
|
|
59
|
+
--wb-surface: var(--color-gray-900, #1a1a1a);
|
|
60
|
+
--wb-fg: var(--color-gray-100, #f5f5f5);
|
|
61
|
+
--wb-muted: var(--color-gray-400, #a1a1a1);
|
|
62
|
+
--wb-border: var(--color-gray-700, #333333);
|
|
63
|
+
--wb-hover: var(--color-gray-800, #262626);
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
body {
|
|
67
67
|
margin: 0;
|
|
68
68
|
padding: 16px;
|
|
69
|
-
background: var(--
|
|
70
|
-
color: var(--
|
|
71
|
-
font-family: var(--
|
|
69
|
+
background: var(--wb-bg);
|
|
70
|
+
color: var(--wb-fg);
|
|
71
|
+
font-family: var(--wb-font);
|
|
72
72
|
font-size: 0.875rem;
|
|
73
73
|
line-height: 1.6;
|
|
74
74
|
}
|
|
@@ -100,13 +100,13 @@ p {
|
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
small,
|
|
103
|
-
.
|
|
104
|
-
color: var(--
|
|
103
|
+
.wb-muted {
|
|
104
|
+
color: var(--wb-muted);
|
|
105
105
|
font-size: 0.8125rem;
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
a {
|
|
109
|
-
color: var(--
|
|
109
|
+
color: var(--wb-accent);
|
|
110
110
|
text-decoration: none;
|
|
111
111
|
}
|
|
112
112
|
a:hover {
|
|
@@ -115,7 +115,7 @@ a:hover {
|
|
|
115
115
|
|
|
116
116
|
hr {
|
|
117
117
|
border: 0;
|
|
118
|
-
border-top: 1px solid var(--
|
|
118
|
+
border-top: 1px solid var(--wb-border);
|
|
119
119
|
margin: 16px 0;
|
|
120
120
|
}
|
|
121
121
|
|
|
@@ -128,40 +128,40 @@ li {
|
|
|
128
128
|
margin: 4px 0;
|
|
129
129
|
}
|
|
130
130
|
/* A list used as a row list rather than prose: the page opts in. */
|
|
131
|
-
ul.
|
|
131
|
+
ul.wb-rows {
|
|
132
132
|
list-style: none;
|
|
133
133
|
padding: 0;
|
|
134
|
-
border: 1px solid var(--
|
|
135
|
-
border-radius: var(--
|
|
134
|
+
border: 1px solid var(--wb-border);
|
|
135
|
+
border-radius: var(--wb-radius);
|
|
136
136
|
}
|
|
137
|
-
ul.
|
|
137
|
+
ul.wb-rows > li {
|
|
138
138
|
margin: 0;
|
|
139
139
|
padding: 8px 12px;
|
|
140
140
|
display: flex;
|
|
141
141
|
align-items: center;
|
|
142
142
|
gap: 8px;
|
|
143
|
-
border-top: 1px solid var(--
|
|
143
|
+
border-top: 1px solid var(--wb-border);
|
|
144
144
|
}
|
|
145
|
-
ul.
|
|
145
|
+
ul.wb-rows > li:first-child {
|
|
146
146
|
border-top: 0;
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
code,
|
|
150
150
|
kbd,
|
|
151
151
|
samp {
|
|
152
|
-
font-family: var(--
|
|
152
|
+
font-family: var(--wb-mono);
|
|
153
153
|
font-size: 0.8125rem;
|
|
154
154
|
}
|
|
155
155
|
code {
|
|
156
|
-
background: var(--
|
|
157
|
-
border: 1px solid var(--
|
|
156
|
+
background: var(--wb-surface);
|
|
157
|
+
border: 1px solid var(--wb-border);
|
|
158
158
|
border-radius: 4px;
|
|
159
159
|
padding: 1px 4px;
|
|
160
160
|
}
|
|
161
161
|
pre {
|
|
162
|
-
background: var(--
|
|
163
|
-
border: 1px solid var(--
|
|
164
|
-
border-radius: var(--
|
|
162
|
+
background: var(--wb-surface);
|
|
163
|
+
border: 1px solid var(--wb-border);
|
|
164
|
+
border-radius: var(--wb-radius);
|
|
165
165
|
padding: 10px 12px;
|
|
166
166
|
overflow-x: auto;
|
|
167
167
|
}
|
|
@@ -178,12 +178,12 @@ table {
|
|
|
178
178
|
}
|
|
179
179
|
th,
|
|
180
180
|
td {
|
|
181
|
-
border-bottom: 1px solid var(--
|
|
181
|
+
border-bottom: 1px solid var(--wb-border);
|
|
182
182
|
padding: 6px 10px;
|
|
183
183
|
text-align: left;
|
|
184
184
|
}
|
|
185
185
|
th {
|
|
186
|
-
color: var(--
|
|
186
|
+
color: var(--wb-muted);
|
|
187
187
|
font-weight: 500;
|
|
188
188
|
font-size: 0.8125rem;
|
|
189
189
|
}
|
|
@@ -193,21 +193,21 @@ select,
|
|
|
193
193
|
textarea {
|
|
194
194
|
font: inherit;
|
|
195
195
|
color: inherit;
|
|
196
|
-
background: var(--
|
|
197
|
-
border: 1px solid var(--
|
|
198
|
-
border-radius: var(--
|
|
196
|
+
background: var(--wb-bg);
|
|
197
|
+
border: 1px solid var(--wb-border);
|
|
198
|
+
border-radius: var(--wb-radius);
|
|
199
199
|
padding: 5px 8px;
|
|
200
200
|
}
|
|
201
201
|
input:focus-visible,
|
|
202
202
|
select:focus-visible,
|
|
203
203
|
textarea:focus-visible,
|
|
204
204
|
button:focus-visible {
|
|
205
|
-
outline: 2px solid var(--
|
|
205
|
+
outline: 2px solid var(--wb-accent);
|
|
206
206
|
outline-offset: 1px;
|
|
207
207
|
}
|
|
208
208
|
input[type="checkbox"],
|
|
209
209
|
input[type="radio"] {
|
|
210
|
-
accent-color: var(--
|
|
210
|
+
accent-color: var(--wb-accent);
|
|
211
211
|
padding: 0;
|
|
212
212
|
}
|
|
213
213
|
|
|
@@ -215,38 +215,38 @@ button {
|
|
|
215
215
|
font: inherit;
|
|
216
216
|
font-weight: 500;
|
|
217
217
|
color: inherit;
|
|
218
|
-
background: var(--
|
|
219
|
-
border: 1px solid var(--
|
|
220
|
-
border-radius: var(--
|
|
218
|
+
background: var(--wb-bg);
|
|
219
|
+
border: 1px solid var(--wb-border);
|
|
220
|
+
border-radius: var(--wb-radius);
|
|
221
221
|
padding: 5px 10px;
|
|
222
222
|
cursor: pointer;
|
|
223
223
|
transition: background-color 150ms;
|
|
224
224
|
}
|
|
225
225
|
button:hover:not(:disabled) {
|
|
226
|
-
background: var(--
|
|
226
|
+
background: var(--wb-hover);
|
|
227
227
|
}
|
|
228
228
|
button:disabled {
|
|
229
229
|
opacity: 0.6;
|
|
230
230
|
cursor: not-allowed;
|
|
231
231
|
}
|
|
232
|
-
button.
|
|
233
|
-
background: var(--
|
|
234
|
-
border-color: var(--
|
|
235
|
-
color: var(--
|
|
232
|
+
button.wb-primary {
|
|
233
|
+
background: var(--wb-accent);
|
|
234
|
+
border-color: var(--wb-accent);
|
|
235
|
+
color: var(--wb-accent-fg);
|
|
236
236
|
}
|
|
237
|
-
button.
|
|
238
|
-
background: var(--
|
|
237
|
+
button.wb-primary:hover:not(:disabled) {
|
|
238
|
+
background: var(--wb-accent);
|
|
239
239
|
opacity: 0.9;
|
|
240
240
|
}
|
|
241
241
|
|
|
242
|
-
.
|
|
243
|
-
background: var(--
|
|
244
|
-
border: 1px solid var(--
|
|
245
|
-
border-radius: var(--
|
|
242
|
+
.wb-card {
|
|
243
|
+
background: var(--wb-surface);
|
|
244
|
+
border: 1px solid var(--wb-border);
|
|
245
|
+
border-radius: var(--wb-radius);
|
|
246
246
|
padding: 12px;
|
|
247
247
|
}
|
|
248
248
|
|
|
249
|
-
.
|
|
249
|
+
.wb-row {
|
|
250
250
|
display: flex;
|
|
251
251
|
align-items: center;
|
|
252
252
|
gap: 8px;
|
|
@@ -259,3 +259,8 @@ button.wf-primary:hover:not(:disabled) {
|
|
|
259
259
|
animation: none !important;
|
|
260
260
|
}
|
|
261
261
|
}
|
|
262
|
+
|
|
263
|
+
/* App layouts must not override the native hidden state of an alert or empty view. */
|
|
264
|
+
[hidden]:not([hidden="until-found"]) {
|
|
265
|
+
display: none !important;
|
|
266
|
+
}
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
/** Why a path cannot go up as a Benchmark package; the caller words it. */
|
|
2
|
-
type BenchmarkUploadProblem = {
|
|
3
|
-
kind: "missing";
|
|
4
|
-
}
|
|
5
|
-
/** Neither a folder nor a file. */
|
|
6
|
-
| {
|
|
7
|
-
kind: "unsupported";
|
|
8
|
-
} | {
|
|
9
|
-
kind: "tooManyFiles";
|
|
10
|
-
limit: number;
|
|
11
|
-
}
|
|
12
|
-
/** `file` is the entry's path inside the folder. */
|
|
13
|
-
| {
|
|
14
|
-
kind: "fileTooLarge";
|
|
15
|
-
file: string;
|
|
16
|
-
limitBytes: number;
|
|
17
|
-
}
|
|
18
|
-
/** The folder's files add up to more than the package may hold. */
|
|
19
|
-
| {
|
|
20
|
-
kind: "tooLarge";
|
|
21
|
-
limitBytes: number;
|
|
22
|
-
}
|
|
23
|
-
/** The zip, given or made, is larger than an upload may be. */
|
|
24
|
-
| {
|
|
25
|
-
kind: "zipTooLarge";
|
|
26
|
-
limitBytes: number;
|
|
27
|
-
};
|
|
28
|
-
declare class BenchmarkUploadError extends Error {
|
|
29
|
-
readonly problem: BenchmarkUploadProblem;
|
|
30
|
-
constructor(problem: BenchmarkUploadProblem);
|
|
31
|
-
}
|
|
32
|
-
/**
|
|
33
|
-
* The bytes to upload for `source`: the zip file itself, or the folder zipped as the module doc
|
|
34
|
-
* says. Throws BenchmarkUploadError for a path that cannot go up (nothing there, past a cap), and
|
|
35
|
-
* whatever the filesystem throws for a path it cannot read.
|
|
36
|
-
*/
|
|
37
|
-
declare function readBenchmarkUpload(source: string): Promise<Uint8Array>;
|
|
38
|
-
|
|
39
|
-
export { BenchmarkUploadError, type BenchmarkUploadProblem, readBenchmarkUpload };
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
MAX_ARCHIVE_BYTES,
|
|
3
|
-
MAX_BENCHMARK_ARCHIVE_FILES,
|
|
4
|
-
MAX_FILE_BYTES,
|
|
5
|
-
MAX_TOTAL_BYTES
|
|
6
|
-
} from "./chunk-54E64LWB.js";
|
|
7
|
-
import "./chunk-Q4ON2L6E.js";
|
|
8
|
-
import "./chunk-B5IP37ZQ.js";
|
|
9
|
-
|
|
10
|
-
// src/benchmark-package.ts
|
|
11
|
-
import fs from "fs/promises";
|
|
12
|
-
import path from "path";
|
|
13
|
-
import { strToU8, zipSync } from "fflate";
|
|
14
|
-
var BenchmarkUploadError = class extends Error {
|
|
15
|
-
constructor(problem) {
|
|
16
|
-
super(`Not a Benchmark package upload: ${problem.kind}`);
|
|
17
|
-
this.problem = problem;
|
|
18
|
-
this.name = "BenchmarkUploadError";
|
|
19
|
-
}
|
|
20
|
-
problem;
|
|
21
|
-
};
|
|
22
|
-
var UNIX_HOST = 3;
|
|
23
|
-
function specialMode(entry) {
|
|
24
|
-
if (entry.isSymbolicLink()) return 41471;
|
|
25
|
-
if (entry.isSocket()) return 49572;
|
|
26
|
-
if (entry.isBlockDevice()) return 24996;
|
|
27
|
-
if (entry.isCharacterDevice()) return 8612;
|
|
28
|
-
return 4516;
|
|
29
|
-
}
|
|
30
|
-
function byName(a, b) {
|
|
31
|
-
return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
|
|
32
|
-
}
|
|
33
|
-
async function readBenchmarkUpload(source) {
|
|
34
|
-
let stat;
|
|
35
|
-
try {
|
|
36
|
-
stat = await fs.stat(source);
|
|
37
|
-
} catch (error) {
|
|
38
|
-
if (error.code === "ENOENT") {
|
|
39
|
-
throw new BenchmarkUploadError({ kind: "missing" });
|
|
40
|
-
}
|
|
41
|
-
throw error;
|
|
42
|
-
}
|
|
43
|
-
const zipTooLarge = () => new BenchmarkUploadError({ kind: "zipTooLarge", limitBytes: MAX_ARCHIVE_BYTES });
|
|
44
|
-
if (stat.isFile()) {
|
|
45
|
-
if (stat.size > MAX_ARCHIVE_BYTES) throw zipTooLarge();
|
|
46
|
-
return new Uint8Array(await fs.readFile(source));
|
|
47
|
-
}
|
|
48
|
-
if (!stat.isDirectory()) throw new BenchmarkUploadError({ kind: "unsupported" });
|
|
49
|
-
const files = {};
|
|
50
|
-
let count = 0;
|
|
51
|
-
let total = 0;
|
|
52
|
-
const walk = async (dir, prefix) => {
|
|
53
|
-
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
54
|
-
for (const entry of entries.sort(byName)) {
|
|
55
|
-
const abs = path.join(dir, entry.name);
|
|
56
|
-
const name = `${prefix}${entry.name}`;
|
|
57
|
-
if (entry.isDirectory()) {
|
|
58
|
-
await walk(abs, `${name}/`);
|
|
59
|
-
continue;
|
|
60
|
-
}
|
|
61
|
-
count += 1;
|
|
62
|
-
if (count > MAX_BENCHMARK_ARCHIVE_FILES) {
|
|
63
|
-
throw new BenchmarkUploadError({
|
|
64
|
-
kind: "tooManyFiles",
|
|
65
|
-
limit: MAX_BENCHMARK_ARCHIVE_FILES
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
if (!entry.isFile()) {
|
|
69
|
-
const target = entry.isSymbolicLink() ? await fs.readlink(abs).catch(() => "") : "";
|
|
70
|
-
files[name] = [strToU8(target), { os: UNIX_HOST, attrs: specialMode(entry) << 16 }];
|
|
71
|
-
continue;
|
|
72
|
-
}
|
|
73
|
-
const size = (await fs.stat(abs)).size;
|
|
74
|
-
if (size > MAX_FILE_BYTES) {
|
|
75
|
-
throw new BenchmarkUploadError({
|
|
76
|
-
kind: "fileTooLarge",
|
|
77
|
-
file: name,
|
|
78
|
-
limitBytes: MAX_FILE_BYTES
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
total += size;
|
|
82
|
-
if (total > MAX_TOTAL_BYTES) {
|
|
83
|
-
throw new BenchmarkUploadError({ kind: "tooLarge", limitBytes: MAX_TOTAL_BYTES });
|
|
84
|
-
}
|
|
85
|
-
files[name] = new Uint8Array(await fs.readFile(abs));
|
|
86
|
-
}
|
|
87
|
-
};
|
|
88
|
-
await walk(source, "");
|
|
89
|
-
const zip = zipSync(files);
|
|
90
|
-
if (zip.byteLength > MAX_ARCHIVE_BYTES) throw zipTooLarge();
|
|
91
|
-
return zip;
|
|
92
|
-
}
|
|
93
|
-
export {
|
|
94
|
-
BenchmarkUploadError,
|
|
95
|
-
readBenchmarkUpload
|
|
96
|
-
};
|
|
97
|
-
//# sourceMappingURL=benchmark-package.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/benchmark-package.ts"],"sourcesContent":["/**\n * A Benchmark package on this machine as the zip `POST /api/projects/:p/benchmarks/archive` takes:\n * what `penguin benchmark import` uploads. A zip file goes as it is. A folder is zipped whole, its\n * entries at the zip's root and nothing left out — dot-entries, stray files and links included, a\n * link as a link — so that the server's checks (services/benchmark-archive.ts) see what the folder\n * holds and refuse by name what a package may not carry. Nothing is trimmed here: an Agent that\n * imports a folder gets the verdict a person uploading its zip would.\n *\n * The folder is held to the import's caps while it is read — 1000 files, 5MB a file, 20MB in all,\n * 14MB zipped — so a wrong path (a whole repository, say) fails at once rather than being read\n * into memory for the server to refuse.\n *\n * Published as `@prismshadow/penguin-server/benchmark-package`, side-effect-free and without the\n * server, for the CLI.\n */\nimport type { Dirent, Stats } from \"node:fs\";\nimport fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { strToU8, zipSync, type Zippable } from \"fflate\";\nimport {\n MAX_ARCHIVE_BYTES,\n MAX_BENCHMARK_ARCHIVE_FILES,\n MAX_FILE_BYTES,\n MAX_TOTAL_BYTES,\n} from \"./services/skill-import-limits.js\";\n\n/** Why a path cannot go up as a Benchmark package; the caller words it. */\nexport type BenchmarkUploadProblem =\n | { kind: \"missing\" }\n /** Neither a folder nor a file. */\n | { kind: \"unsupported\" }\n | { kind: \"tooManyFiles\"; limit: number }\n /** `file` is the entry's path inside the folder. */\n | { kind: \"fileTooLarge\"; file: string; limitBytes: number }\n /** The folder's files add up to more than the package may hold. */\n | { kind: \"tooLarge\"; limitBytes: number }\n /** The zip, given or made, is larger than an upload may be. */\n | { kind: \"zipTooLarge\"; limitBytes: number };\n\nexport class BenchmarkUploadError extends Error {\n constructor(readonly problem: BenchmarkUploadProblem) {\n super(`Not a Benchmark package upload: ${problem.kind}`);\n this.name = \"BenchmarkUploadError\";\n }\n}\n\n/** The zip format's host id for Unix: an entry's high attribute bits are then its `st_mode`. */\nconst UNIX_HOST = 3;\n\n/** The `st_mode` an entry that is neither a file nor a directory is recorded with. */\nfunction specialMode(entry: Dirent): number {\n if (entry.isSymbolicLink()) return 0o120777;\n if (entry.isSocket()) return 0o140644;\n if (entry.isBlockDevice()) return 0o060644;\n if (entry.isCharacterDevice()) return 0o020644;\n return 0o010644; // a FIFO, or a type this platform does not name\n}\n\n/** Code-unit order: the same zip from the same folder, whatever the locale. */\nfunction byName(a: Dirent, b: Dirent): number {\n return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;\n}\n\n/**\n * The bytes to upload for `source`: the zip file itself, or the folder zipped as the module doc\n * says. Throws BenchmarkUploadError for a path that cannot go up (nothing there, past a cap), and\n * whatever the filesystem throws for a path it cannot read.\n */\nexport async function readBenchmarkUpload(source: string): Promise<Uint8Array> {\n let stat: Stats;\n try {\n stat = await fs.stat(source);\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") {\n throw new BenchmarkUploadError({ kind: \"missing\" });\n }\n throw error;\n }\n const zipTooLarge = () =>\n new BenchmarkUploadError({ kind: \"zipTooLarge\", limitBytes: MAX_ARCHIVE_BYTES });\n if (stat.isFile()) {\n if (stat.size > MAX_ARCHIVE_BYTES) throw zipTooLarge();\n return new Uint8Array(await fs.readFile(source));\n }\n if (!stat.isDirectory()) throw new BenchmarkUploadError({ kind: \"unsupported\" });\n\n const files: Zippable = {};\n let count = 0;\n let total = 0;\n const walk = async (dir: string, prefix: string): Promise<void> => {\n const entries = await fs.readdir(dir, { withFileTypes: true });\n for (const entry of entries.sort(byName)) {\n const abs = path.join(dir, entry.name);\n const name = `${prefix}${entry.name}`;\n if (entry.isDirectory()) {\n await walk(abs, `${name}/`);\n continue;\n }\n count += 1;\n if (count > MAX_BENCHMARK_ARCHIVE_FILES) {\n throw new BenchmarkUploadError({\n kind: \"tooManyFiles\",\n limit: MAX_BENCHMARK_ARCHIVE_FILES,\n });\n }\n if (!entry.isFile()) {\n // A link or a special file goes in as what it is, its type in the entry's mode and a\n // link's target as its content, for the server to refuse by name: never followed, never\n // read.\n const target = entry.isSymbolicLink() ? await fs.readlink(abs).catch(() => \"\") : \"\";\n files[name] = [strToU8(target), { os: UNIX_HOST, attrs: specialMode(entry) << 16 }];\n continue;\n }\n const size = (await fs.stat(abs)).size;\n if (size > MAX_FILE_BYTES) {\n throw new BenchmarkUploadError({\n kind: \"fileTooLarge\",\n file: name,\n limitBytes: MAX_FILE_BYTES,\n });\n }\n total += size;\n if (total > MAX_TOTAL_BYTES) {\n throw new BenchmarkUploadError({ kind: \"tooLarge\", limitBytes: MAX_TOTAL_BYTES });\n }\n files[name] = new Uint8Array(await fs.readFile(abs));\n }\n };\n await walk(source, \"\");\n const zip = zipSync(files);\n if (zip.byteLength > MAX_ARCHIVE_BYTES) throw zipTooLarge();\n return zip;\n}\n"],"mappings":"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","names":[]}
|
package/dist/chunk-54E64LWB.js
DELETED
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
HttpError,
|
|
3
|
-
badRequest
|
|
4
|
-
} from "./chunk-Q4ON2L6E.js";
|
|
5
|
-
|
|
6
|
-
// src/services/skill-import-limits.ts
|
|
7
|
-
import { unzipSync } from "fflate";
|
|
8
|
-
var MAX_ARCHIVE_BYTES = 14 * 1024 * 1024;
|
|
9
|
-
var MAX_ARCHIVE_FILES = 200;
|
|
10
|
-
var MAX_FILE_BYTES = 5 * 1024 * 1024;
|
|
11
|
-
var MAX_TOTAL_BYTES = 20 * 1024 * 1024;
|
|
12
|
-
var MAX_BENCHMARK_ARCHIVE_FILES = 1e3;
|
|
13
|
-
function assertSafeEntryPath(name) {
|
|
14
|
-
if (name.includes("\\")) throw badRequest(`Invalid zip entry path (backslash): ${name}`);
|
|
15
|
-
if (name.startsWith("/") || /^[A-Za-z]:/.test(name)) {
|
|
16
|
-
throw badRequest(`Invalid zip entry path (absolute): ${name}`);
|
|
17
|
-
}
|
|
18
|
-
if (name.split("/").some((segment) => segment === "..")) {
|
|
19
|
-
throw badRequest(`Invalid zip entry path (traversal): ${name}`);
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
function unzipBounded(archive, limits = {}) {
|
|
23
|
-
const maxFiles = limits.maxFiles ?? MAX_ARCHIVE_FILES;
|
|
24
|
-
const tooLarge = limits.tooLarge ?? badRequest;
|
|
25
|
-
let files = 0;
|
|
26
|
-
let declared = 0;
|
|
27
|
-
return unzipSync(archive, {
|
|
28
|
-
filter: ({ name, originalSize }) => {
|
|
29
|
-
if (name.endsWith("/")) return true;
|
|
30
|
-
files += 1;
|
|
31
|
-
if (files > maxFiles) {
|
|
32
|
-
throw tooLarge(`The zip archive exceeds the ${maxFiles}-file limit.`);
|
|
33
|
-
}
|
|
34
|
-
if (originalSize > MAX_FILE_BYTES) {
|
|
35
|
-
throw tooLarge(`Zip entry exceeds the 5MB uncompressed limit: ${name}`);
|
|
36
|
-
}
|
|
37
|
-
declared += originalSize;
|
|
38
|
-
if (declared > MAX_TOTAL_BYTES) {
|
|
39
|
-
throw tooLarge("The zip archive exceeds the 20MB uncompressed limit.");
|
|
40
|
-
}
|
|
41
|
-
return true;
|
|
42
|
-
}
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
function skillTooLarge() {
|
|
46
|
-
return new HttpError(
|
|
47
|
-
413,
|
|
48
|
-
"skill_too_large",
|
|
49
|
-
`Skill directory exceeds the archive limits (${MAX_ARCHIVE_FILES} files, 5MB per file, 20MB total).`
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export {
|
|
54
|
-
MAX_ARCHIVE_BYTES,
|
|
55
|
-
MAX_ARCHIVE_FILES,
|
|
56
|
-
MAX_FILE_BYTES,
|
|
57
|
-
MAX_TOTAL_BYTES,
|
|
58
|
-
MAX_BENCHMARK_ARCHIVE_FILES,
|
|
59
|
-
assertSafeEntryPath,
|
|
60
|
-
unzipBounded,
|
|
61
|
-
skillTooLarge
|
|
62
|
-
};
|
|
63
|
-
//# sourceMappingURL=chunk-54E64LWB.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/services/skill-import-limits.ts"],"sourcesContent":["/**\n * Caps shared by every path that pulls Skill files in from outside the library — the archive\n * upload/download routes and the directory import. One set of numbers, so a limit raised for one\n * entry point cannot quietly leave another behind, and one message, so the failure reads the same\n * wherever the user hit it. `unzipBounded` is where they are enforced on the way in, since an\n * archive can only be bounded before it is inflated. The Benchmark package archive\n * (benchmark-archive.ts) shares them all but the file count.\n *\n * The CLI loads this module too, through benchmark-package.ts, to hold a folder it uploads to the\n * same caps: it imports fflate and errors.ts and nothing heavier, and keeps it that way.\n */\nimport { unzipSync } from \"fflate\";\nimport { HttpError, badRequest } from \"../http/errors.js\";\n\n/** Decoded zip cap: aligned with the Agent snapshot import (stays within the 20MB body limit after base64). Shared with the hooks and Benchmark archive routes. */\nexport const MAX_ARCHIVE_BYTES = 14 * 1024 * 1024;\nexport const MAX_ARCHIVE_FILES = 200;\nexport const MAX_FILE_BYTES = 5 * 1024 * 1024;\nexport const MAX_TOTAL_BYTES = 20 * 1024 * 1024;\n\n/**\n * A Benchmark package's file cap. Each case carries its statement's and its rubric's materials,\n * so a package of ten cases outgrows the Skill cap long before it nears the byte caps, which it\n * shares.\n */\nexport const MAX_BENCHMARK_ARCHIVE_FILES = 1000;\n\n/**\n * Validates one zip entry path (zip-slip guard): rejects absolute paths (leading \"/\" or a\n * drive letter), backslashes and any \"..\" segment — a malicious archive must never write\n * outside the target directory. Shared by the Skill, hook and Benchmark archive imports.\n */\nexport function assertSafeEntryPath(name: string): void {\n if (name.includes(\"\\\\\")) throw badRequest(`Invalid zip entry path (backslash): ${name}`);\n if (name.startsWith(\"/\") || /^[A-Za-z]:/.test(name)) {\n throw badRequest(`Invalid zip entry path (absolute): ${name}`);\n }\n if (name.split(\"/\").some((segment) => segment === \"..\")) {\n throw badRequest(`Invalid zip entry path (traversal): ${name}`);\n }\n}\n\n/** What a caller may change about `unzipBounded`'s caps. */\nexport interface UnzipLimits {\n /** Files the archive may hold; MAX_ARCHIVE_FILES when absent. */\n maxFiles?: number;\n /** The error a broken cap throws, given its message; a 400 when absent. */\n tooLarge?: (message: string) => HttpError;\n}\n\n/**\n * `unzipSync` with the caps above enforced from the central directory, before a byte is inflated.\n *\n * They have to be read there, because `unzipSync` allocates a buffer of each entry's declared\n * uncompressed size and inflates into it — so caps measured on what comes back bound nothing.\n * Zeros deflate at roughly 1000:1, which turns the largest archive the route accepts into\n * gigabytes of allocation; and an entry whose header overstates its size hands back a short view\n * onto a buffer of the declared length, passing every after-the-fact byte check outright.\n *\n * Bounding against the declared sizes is sound in the other direction too: the buffer is exactly\n * the declared size and fflate never grows it, so an entry can only ever yield fewer bytes than\n * it claimed. Directory entries carry no content and are left to the caller to filter.\n *\n * Throws the caps as 400s unless `limits.tooLarge` says otherwise, matching the archive route this\n * was factored out of; a corrupt archive still throws whatever fflate throws, for the caller to\n * translate.\n */\nexport function unzipBounded(\n archive: Uint8Array,\n limits: UnzipLimits = {},\n): Record<string, Uint8Array> {\n const maxFiles = limits.maxFiles ?? MAX_ARCHIVE_FILES;\n const tooLarge = limits.tooLarge ?? badRequest;\n let files = 0;\n let declared = 0;\n return unzipSync(archive, {\n filter: ({ name, originalSize }) => {\n if (name.endsWith(\"/\")) return true;\n files += 1;\n if (files > maxFiles) {\n throw tooLarge(`The zip archive exceeds the ${maxFiles}-file limit.`);\n }\n if (originalSize > MAX_FILE_BYTES) {\n throw tooLarge(`Zip entry exceeds the 5MB uncompressed limit: ${name}`);\n }\n declared += originalSize;\n if (declared > MAX_TOTAL_BYTES) {\n throw tooLarge(\"The zip archive exceeds the 20MB uncompressed limit.\");\n }\n return true;\n },\n });\n}\n\nexport function skillTooLarge(): HttpError {\n return new HttpError(\n 413,\n \"skill_too_large\",\n `Skill directory exceeds the archive limits (${MAX_ARCHIVE_FILES} files, 5MB per file, 20MB total).`,\n );\n}\n"],"mappings":";;;;;;AAWA,SAAS,iBAAiB;AAInB,IAAM,oBAAoB,KAAK,OAAO;AACtC,IAAM,oBAAoB;AAC1B,IAAM,iBAAiB,IAAI,OAAO;AAClC,IAAM,kBAAkB,KAAK,OAAO;AAOpC,IAAM,8BAA8B;AAOpC,SAAS,oBAAoB,MAAoB;AACtD,MAAI,KAAK,SAAS,IAAI,EAAG,OAAM,WAAW,uCAAuC,IAAI,EAAE;AACvF,MAAI,KAAK,WAAW,GAAG,KAAK,aAAa,KAAK,IAAI,GAAG;AACnD,UAAM,WAAW,sCAAsC,IAAI,EAAE;AAAA,EAC/D;AACA,MAAI,KAAK,MAAM,GAAG,EAAE,KAAK,CAAC,YAAY,YAAY,IAAI,GAAG;AACvD,UAAM,WAAW,uCAAuC,IAAI,EAAE;AAAA,EAChE;AACF;AA2BO,SAAS,aACd,SACA,SAAsB,CAAC,GACK;AAC5B,QAAM,WAAW,OAAO,YAAY;AACpC,QAAM,WAAW,OAAO,YAAY;AACpC,MAAI,QAAQ;AACZ,MAAI,WAAW;AACf,SAAO,UAAU,SAAS;AAAA,IACxB,QAAQ,CAAC,EAAE,MAAM,aAAa,MAAM;AAClC,UAAI,KAAK,SAAS,GAAG,EAAG,QAAO;AAC/B,eAAS;AACT,UAAI,QAAQ,UAAU;AACpB,cAAM,SAAS,+BAA+B,QAAQ,cAAc;AAAA,MACtE;AACA,UAAI,eAAe,gBAAgB;AACjC,cAAM,SAAS,iDAAiD,IAAI,EAAE;AAAA,MACxE;AACA,kBAAY;AACZ,UAAI,WAAW,iBAAiB;AAC9B,cAAM,SAAS,sDAAsD;AAAA,MACvE;AACA,aAAO;AAAA,IACT;AAAA,EACF,CAAC;AACH;AAEO,SAAS,gBAA2B;AACzC,SAAO,IAAI;AAAA,IACT;AAAA,IACA;AAAA,IACA,+CAA+C,iBAAiB;AAAA,EAClE;AACF;","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/auth/password.ts","../src/auth/service.ts","../src/auth/api-token.ts"],"sourcesContent":["import { Component, Interface } from \"@prismshadow/penguin-core/kernel\";\n/**\n * Password hashing (slow-hash storage).\n *\n * Uses node:crypto's scrypt (built-in, no extra dependency, meets the same\n * slow-hash requirement as bcrypt/argon2). Storage format:\n * `scrypt$N$r$p$<salt b64>$<hash b64>` — parameters are stored alongside the hash,\n * so old hashes remain verifiable after future parameter tuning; comparison uses\n * timingSafeEqual to guard against timing side-channels, and a sign-in for an account\n * that does not exist costs the same derivation as a wrong password\n * (verifyAccountPassword).\n */\nimport { randomBytes, scrypt, timingSafeEqual } from \"node:crypto\";\n\n/**\n * Work factor for new hashes. `hashPassword` takes it as a defaulted argument so the server's\n * own test suite can hash at a token cost: almost every case there seeds an admin, provisions\n * users and logs them in, and one derivation at this strength costs on the order of 100ms.\n * Nothing on the production path passes the argument, and no configuration reaches it —\n * `buildAppDeps`, which carries the test override, is not part of this package's exports.\n */\nexport const SCRYPT_COST = 16384;\nconst SCRYPT_R = 8;\nconst SCRYPT_P = 1;\nconst SALT_BYTES = 16;\nconst KEY_BYTES = 64;\n\nfunction scryptAsync(\n password: string,\n salt: Buffer,\n keyLen: number,\n n: number,\n r: number,\n p: number,\n): Promise<Buffer> {\n return new Promise((resolve, reject) => {\n scrypt(password, salt, keyLen, { N: n, r, p, maxmem: 128 * 1024 * 1024 }, (err, key) => {\n if (err) reject(err);\n else resolve(key);\n });\n });\n}\n\n/**\n * Generates a password hash in `scrypt$N$r$p$salt$hash` format. `cost` is scrypt's N and\n * is recorded in the hash, so `verifyPassword` re-derives at whatever cost produced the\n * stored string.\n */\nexport async function hashPassword(password: string, cost: number = SCRYPT_COST): Promise<string> {\n const salt = randomBytes(SALT_BYTES);\n const key = await scryptAsync(password, salt, KEY_BYTES, cost, SCRYPT_R, SCRYPT_P);\n return [\n \"scrypt\",\n String(cost),\n String(SCRYPT_R),\n String(SCRYPT_P),\n salt.toString(\"base64\"),\n key.toString(\"base64\"),\n ].join(\"$\");\n}\n\n/**\n * What checking a password against a stored string found. `match` and `mismatch` each cost one\n * scrypt derivation; `unverifiable` — not the `scrypt$N$r$p$salt$hash` format, or parameters\n * scrypt refuses — cost none.\n */\nexport type PasswordCheck = \"match\" | \"mismatch\" | \"unverifiable\";\n\n/** Checks a password against a stored hash. Never throws. */\nexport async function checkPassword(password: string, stored: string): Promise<PasswordCheck> {\n const parts = stored.split(\"$\");\n if (parts.length !== 6 || parts[0] !== \"scrypt\") return \"unverifiable\";\n const n = Number(parts[1]);\n const r = Number(parts[2]);\n const p = Number(parts[3]);\n if (!Number.isInteger(n) || !Number.isInteger(r) || !Number.isInteger(p)) return \"unverifiable\";\n const salt = Buffer.from(parts[4]!, \"base64\");\n const expected = Buffer.from(parts[5]!, \"base64\");\n if (salt.length === 0 || expected.length === 0) return \"unverifiable\";\n let actual: Buffer;\n try {\n actual = await scryptAsync(password, salt, expected.length, n, r, p);\n } catch {\n return \"unverifiable\";\n }\n return timingSafeEqual(actual, expected) ? \"match\" : \"mismatch\";\n}\n\n/** Verifies a password; returns false if the stored string has an invalid format (never throws, so the login path can uniformly treat it as a credential error). */\nexport async function verifyPassword(password: string, stored: string): Promise<boolean> {\n return (await checkPassword(password, stored)) === \"match\";\n}\n\n/**\n * Verifies a sign-in password for an account that may not exist, at the cost a real account's\n * check has. `stored` is the account's hash, or null when there is no such account. When there is\n * nothing to derive against — no account, an empty hash, or one `checkPassword` finds\n * unverifiable — the password is checked against `dummyHash` instead and the result is false, so\n * every call runs exactly one scrypt derivation and how long a failure takes does not tell a\n * missing username from a wrong password. `check` is a parameter so a test can count the\n * derivations.\n */\nexport async function verifyAccountPassword(\n password: string,\n stored: string | null,\n dummyHash: () => Promise<string>,\n check: (password: string, stored: string) => Promise<PasswordCheck> = checkPassword,\n): Promise<boolean> {\n const outcome = stored === null ? \"unverifiable\" : await check(password, stored);\n if (outcome !== \"unverifiable\") return outcome === \"match\";\n await check(password, await dummyHash());\n return false;\n}\n\n/** Hashes the passwords this server writes; a test stands in a cheap one. */\n@Interface()\nexport abstract class PasswordHasher {\n abstract hash(password: string): Promise<string>;\n}\n\n/** scrypt at full strength. */\n@Component()\nexport class ScryptHasher implements PasswordHasher {\n hash(password: string): Promise<string> {\n return hashPassword(password, SCRYPT_COST);\n }\n}\n","/**\n * Auth service: admin seeding / login / logout / password change / session validation.\n *\n * No open registration: an empty users table seeds `admin` with a random password hashed and\n * discarded unseen, claimed through the first-login link; every other user is created by an\n * admin. Sessions are rows in auth_sessions (cookie holds the token, the row its sha256), so\n * they outlive a restart and renew in place.\n */\nimport { randomBytes } from \"node:crypto\";\nimport { tokensEqual } from \"./api-token.js\";\nimport type { UserInfo } from \"../api/types.js\";\nimport { HttpError } from \"../http/errors.js\";\nimport type { UserRow } from \"../db/repos/users.js\";\nimport { sessionTokenHash } from \"../db/repos/auth-sessions.js\";\nimport type { SessionViaValue, AuthSessionsRepo } from \"../db/repos/auth-sessions.js\";\nimport type { AuthRuntimeState } from \"./runtime-state.js\";\nimport { verifyAccountPassword, verifyPassword } from \"./password.js\";\nimport type { PasswordHasher } from \"./password.js\";\nimport { Component, Use, Interface } from \"@prismshadow/penguin-core/kernel\";\nimport type { AuthState, Clock, Config } from \"../hmr/capabilities.js\";\nimport type { Auth, AuthSessions, Users } from \"../mechanisms/identity.js\";\n\nexport const MIN_PASSWORD_LENGTH = 8;\n\n/** Built-in admin user_id. */\nexport const ADMIN_USER_ID = \"admin\";\n\n/**\n * Login throttling, per userId. After LOGIN_FREE_ATTEMPTS failures each attempt waits an\n * exponentially growing delay (1s doubling to a 60s cap), settling at a guess per minute.\n * Kept for nonexistent userIds too, so it is not an account-existence oracle — and neither is\n * the time a failure takes: a nonexistent userId is checked against a dummy hash (loginDummyHash).\n * Deliberately NOT covered: a concurrent burst before the first failure lands, and guesses\n * spread across many accounts.\n */\nconst LOGIN_FREE_ATTEMPTS = 5;\nconst LOGIN_BACKOFF_START_MS = 1000;\nconst LOGIN_BACKOFF_CAP_MS = 60_000;\n/** Failure entries idle longer than this are swept (bounds the map; far above the cap). */\nconst LOGIN_FAILURE_IDLE_MS = 15 * 60_000;\n\n/** Characters of base64url in a seeded password — 18 random bytes, 144 bits. */\nconst SEED_PASSWORD_CHARS = 24;\n\n/**\n * Never read or typed — hashed at once and discarded — which is what lets it be long enough\n * that the login endpoint, reachable the moment the account exists, has no search space.\n */\nexport function generateInitialAdminPassword(): string {\n return randomBytes(SEED_PASSWORD_CHARS).toString(\"base64url\").slice(0, SEED_PASSWORD_CHARS);\n}\n\n/**\n * \"desktop\" and \"setup\" may set a password without the old one (theirs is random and was never\n * shown); desktop-only routes open to \"desktop\" alone. \"token\" is the local API token's Bearer\n * header — per request, never a stored row. Anything else reads as \"password\".\n */\nexport type SessionVia = \"password\" | \"desktop\" | \"setup\" | \"token\";\n\n/** Row -> DTO. The two profile columns are omitted rather than sent as null when unset. */\nexport function toUserInfo(row: UserRow): UserInfo {\n return {\n userId: row.userId,\n isAdmin: row.isAdmin,\n passwordIsInitial: row.passwordIsInitial,\n ...(row.displayName !== null ? { displayName: row.displayName } : {}),\n ...(row.avatar !== null ? { avatar: row.avatar } : {}),\n createdAt: row.createdAt,\n };\n}\n\n@Component()\nexport class AuthService implements Auth {\n @Use() private readonly users!: Users;\n @Use() private readonly authSessions!: AuthSessions;\n /**\n * Process-scoped auth values (runtime-state.ts). Held by the RUNTIME so the link a boot\n * printed survives a platform push; everything else about auth is rebuilt with the App.\n */\n @Use() private readonly state!: AuthState;\n /** Provisions the initial Project at signup — declared at the consumer, below. */\n @Use() private readonly provisioner!: InitialProjectProvisioner;\n @Use() private readonly config!: Config;\n @Use() private readonly clock!: Clock;\n @Use() private readonly hasher!: PasswordHasher;\n /** Session lifetime, for the cookie that must expire WITH the session, not before it. */\n get sessionTtlMs(): number {\n return this.config.authSessionTtlMs;\n }\n private get sessionRenewMs(): number {\n return this.config.authSessionRenewMs;\n }\n /** Fixed initial password for the seeded admin; null generates a random one at seed time. */\n private get seedAdminPassword(): string | null {\n return this.config.seedAdminPassword;\n }\n\n setup(): void {\n this.authSessions.deleteExpired(this.clock.now().toISOString());\n }\n\n /**\n * Null once the server is claimed, so the only setup session that exists is a printed one.\n * On demand rather than in the constructor: seedAdmin() runs after it, so \"claimed?\" has no\n * answer there. A cached link whose row is gone is re-minted, not handed out dead.\n */\n mintFirstLogin(): string | null {\n if (!this.adminPasswordIsInitial()) return null;\n if (\n this.state.firstLoginToken !== null &&\n this.authenticateWithMeta(this.state.firstLoginToken) === null\n ) {\n this.state.firstLoginToken = null;\n }\n this.state.firstLoginToken ??= this.issueSession(ADMIN_USER_ID, \"setup\");\n return this.state.firstLoginToken;\n }\n\n /**\n * For the startup notice's pinned-seed gate (index.ts): a pin usually means the operator\n * knows the password, but an offline reset makes it stale, and the gate must see through\n * that or the rescue link stays suppressed.\n */\n async adminPasswordIs(password: string): Promise<boolean> {\n const row = this.users.findById(ADMIN_USER_ID);\n return row !== null && (await verifyPassword(password, row.passwordHash));\n }\n\n /**\n * Startup seeding (idempotent): creates the built-in admin and adopts default_project when\n * the users table is empty; if the initial Project fails, the user row is rolled back and\n * the server retries on next startup.\n */\n async seedAdmin(): Promise<void> {\n if (this.users.count() > 0) return;\n const password = this.seedAdminPassword ?? generateInitialAdminPassword();\n // A pinned override must meet the same policy as every other password — rejected before\n // any insert, so a configuration typo cannot create a trivially weak privileged account.\n if (password.length < MIN_PASSWORD_LENGTH) {\n throw new Error(\n `PENGUIN_SEED_ADMIN_PASSWORD must be at least ${MIN_PASSWORD_LENGTH} characters.`,\n );\n }\n const user: UserRow = {\n userId: ADMIN_USER_ID,\n passwordHash: await this.hasher.hash(password),\n isAdmin: true,\n passwordIsInitial: true,\n displayName: null,\n avatar: null,\n createdAt: this.clock.now().toISOString(),\n };\n this.users.insert(user);\n try {\n await this.provisioner.provisionInitialProject(user, true);\n } catch (err) {\n this.users.delete(user.userId);\n throw err;\n }\n }\n\n /**\n * Compared against the printed value, not merely verified: a cookie made out of ANY valid\n * token would let one person hand another a link into the sender's account. Not single-use,\n * because a prefetching browser would burn it before its reader clicked.\n */\n redeemFirstLogin(given: string): string | null {\n const expected = this.state.firstLoginToken;\n if (expected === null || given === \"\" || !tokensEqual(given, expected)) return null;\n return this.authenticateWithMeta(expected) === null ? null : expected;\n }\n\n /** Whether the built-in admin still runs on its initial password (gates the first-login link). */\n adminPasswordIsInitial(): boolean {\n return this.users.findById(ADMIN_USER_ID)?.passwordIsInitial === true;\n }\n\n /** Consecutive login failures per userId (see the throttling comment on the constants). */\n private readonly loginFailures = new Map<string, { failures: number; lastFailureAt: number }>();\n\n /** Cache for loginDummyHash. */\n private dummyHash: string | null = null;\n\n /**\n * The hash a sign-in with no account to check is verified against (verifyAccountPassword), so\n * it costs what a wrong password costs. Made by this server's own hasher, hence at the cost its\n * real hashes carry, from a random password nobody holds; computed on the first such sign-in\n * and kept.\n */\n private async loginDummyHash(): Promise<string> {\n this.dummyHash ??= await this.hasher.hash(randomBytes(18).toString(\"base64url\"));\n return this.dummyHash;\n }\n\n /** The wait imposed after `failures` consecutive failures (0 while within the free attempts). */\n private loginDelayMs(failures: number): number {\n const excess = failures - LOGIN_FREE_ATTEMPTS;\n if (excess <= 0) return 0;\n return Math.min(LOGIN_BACKOFF_START_MS * 2 ** (excess - 1), LOGIN_BACKOFF_CAP_MS);\n }\n\n async login(userId: string, password: string): Promise<{ user: UserInfo; token: string }> {\n const nowMs = this.clock.now().getTime();\n for (const [key, entry] of this.loginFailures) {\n if (nowMs - entry.lastFailureAt > LOGIN_FAILURE_IDLE_MS) this.loginFailures.delete(key);\n }\n const failed = this.loginFailures.get(userId);\n if (failed) {\n const readyAt = failed.lastFailureAt + this.loginDelayMs(failed.failures);\n if (nowMs < readyAt) {\n throw new HttpError(\n 429,\n \"too_many_attempts\",\n `Too many failed sign-in attempts. Try again in ${Math.ceil((readyAt - nowMs) / 1000)}s.`,\n );\n }\n }\n const row = this.users.findById(userId);\n // A missing account, or one whose hash cannot be checked, still costs one scrypt\n // derivation, and fails with the same 401 as a wrong password.\n const ok = await verifyAccountPassword(password, row?.passwordHash ?? null, () =>\n this.loginDummyHash(),\n );\n if (!row || !ok) {\n this.loginFailures.set(userId, {\n failures: (failed?.failures ?? 0) + 1,\n lastFailureAt: this.clock.now().getTime(),\n });\n throw new HttpError(401, \"invalid_credentials\", \"Incorrect username or password.\");\n }\n this.loginFailures.delete(userId);\n this.authSessions.deleteExpired(this.clock.now().toISOString());\n return { user: toUserInfo(row), token: this.issueSession(row.userId, \"password\") };\n }\n\n /**\n * Whether a user is an admin, by id — for a caller that has a user's NAME rather than an\n * authenticated request: the terminal relay reads the owner out of a pty reference the\n * runtime has already matched against the signed-in user (machines/terminal-relay.ts), and\n * what is left to decide is whether that user may reach machines at all. Unknown users are\n * not admins.\n */\n isAdmin(userId: string): boolean {\n return this.users.findById(userId)?.isAdmin === true;\n }\n\n /**\n * Desktop-mode sign-in: an admin session with no password check — the claim route already\n * redeemed the shell's one-shot token, which is the credential here. Throws only on a\n * broken deployment (seeding runs before the route exists).\n */\n loginDesktop(): { user: UserInfo; token: string } {\n const row = this.users.findById(ADMIN_USER_ID);\n if (!row) {\n throw new HttpError(500, \"internal\", \"Built-in admin has not been seeded.\");\n }\n return { user: toUserInfo(row), token: this.issueSession(row.userId, \"desktop\") };\n }\n\n /** Self password change (user settings): validates the old password first; the current session stays valid. */\n async changePassword(userId: string, oldPassword: string, newPassword: string): Promise<void> {\n const row = this.users.findById(userId);\n if (!row || !(await verifyPassword(oldPassword, row.passwordHash))) {\n throw new HttpError(400, \"password_mismatch\", \"Current password is incorrect.\");\n }\n await this.setPassword(userId, newPassword);\n }\n\n /** No old-password check; the me route gates which sessions may call this. */\n async setInitialPassword(userId: string, newPassword: string): Promise<void> {\n await this.setPassword(userId, newPassword);\n }\n\n /**\n * Validation comes FIRST, so a rejected password leaves the first-login link alive: burning\n * it on a typo would strand the claimer until a restart.\n */\n private async setPassword(userId: string, newPassword: string): Promise<void> {\n if (newPassword.length < MIN_PASSWORD_LENGTH) {\n throw new HttpError(400, \"invalid_password\", \"Password must be at least 8 characters.\");\n }\n this.users.updatePassword(userId, await this.hasher.hash(newPassword), false);\n // A successful set proves at least what the successful login that resets the counter\n // proves (the old password, or a desktop/setup session), so it resets it too. Without\n // this, anyone spamming the login endpoint holds the backoff window open, and the sign-in\n // that follows a first-login claim (routes/me.ts) would 429 AFTER the password committed —\n // reporting failure for a change that took, with the claimer's setup session already gone.\n this.loginFailures.delete(userId);\n // EVERY first-login session for this account, not just the link this process printed: an\n // earlier boot's link can still be live in a terminal scrollback, and a `setup` session is\n // allowed to change the password without knowing the old one — so one left behind is an\n // account takeover waiting to happen.\n if (userId === ADMIN_USER_ID) {\n this.authSessions.deleteByUserAndVia(userId, \"setup\");\n this.state.firstLoginToken = null;\n }\n }\n\n /** Ends a session: the row is the session, so logout deletes it. Unknown token is a no-op. */\n logout(token: string): void {\n this.authSessions.delete(sessionTokenHash(token));\n }\n\n /**\n * The current boot's local API token (what control-env injection hands to tool\n * subprocesses); null when none was minted. It lives on the runtime state, not on this\n * service: the token was written to `<root>/api-token` by the process, so it must\n * outlive the App a push replaces (auth/runtime-state.ts).\n */\n localApiToken(): string | null {\n return this.state.apiToken;\n }\n\n /**\n * Validates a Bearer token against the boot's local API token (constant-time compare):\n * a match authenticates as the built-in admin — holding the token proves filesystem\n * access to the data root, which is admin authority (see auth/api-token.ts). Returns\n * null on mismatch, when no token was minted, or when the admin row is missing.\n */\n authenticateApiToken(token: string): { user: UserRow; via: SessionVia } | null {\n const apiToken = this.state.apiToken;\n if (apiToken === null || token.length === 0) return null;\n if (!tokensEqual(token, apiToken)) return null;\n const user = this.users.findById(ADMIN_USER_ID);\n return user === null ? null : { user, via: \"token\" };\n }\n\n /**\n * Validates the cookie token. Sliding renewal tops the expiry up IN PLACE, so the cookie\n * value never changes and there is no second identity to revoke. Only a session whose own\n * span reaches the renewal window slides — a one-hour minted token must expire at its hour,\n * not stretch by being used.\n */\n authenticateWithMeta(token: string): { user: UserRow; via: SessionVia; renewed: boolean } | null {\n const tokenHash = sessionTokenHash(token);\n const session = this.authSessions.findByTokenHash(tokenHash);\n if (!session) return null;\n const now = this.clock.now();\n const expiresAt = Date.parse(session.expiresAt);\n if (expiresAt <= now.getTime()) {\n this.authSessions.delete(tokenHash);\n return null;\n }\n const user = this.users.findById(session.userId);\n if (!user) return null;\n const renewable = expiresAt - Date.parse(session.createdAt) >= this.sessionRenewMs;\n let renewed = false;\n if (renewable && expiresAt - now.getTime() < this.sessionRenewMs) {\n this.authSessions.touch(tokenHash, new Date(now.getTime() + this.sessionTtlMs).toISOString());\n renewed = true;\n }\n const via: SessionVia =\n session.via === \"desktop\" ? \"desktop\" : session.via === \"setup\" ? \"setup\" : \"password\";\n return { user, via, renewed };\n }\n\n /**\n * Whether the session behind a cookie token still exists and has not expired — the same\n * two facts {@link authenticateWithMeta} establishes, without its sliding renewal or its\n * user lookup. A connection that outlives the request that opened it has to re-ask\n * periodically (the SSE heartbeat does, once per beat), and asking through the renewing\n * path would top the expiry up every time: a window nobody has touched since last week\n * would stay signed in purely because it is still connected.\n */\n sessionIsLive(token: string): boolean {\n const session = this.authSessions.findByTokenHash(sessionTokenHash(token));\n if (!session) return false;\n return Date.parse(session.expiresAt) > this.clock.now().getTime();\n }\n\n private issueSession(userId: string, via: SessionViaValue): string {\n return this.authSessions.issue({\n userId,\n via,\n now: this.clock.now(),\n ttlMs: this.sessionTtlMs,\n }).token;\n }\n}\n\n/** What signing a new user in needs of project administration — declared at the consumer. */\n@Interface()\nexport abstract class InitialProjectProvisioner {\n abstract provisionInitialProject(user: UserRow, isAdmin: boolean): Promise<void>;\n}\n","/**\n * Local API token (`<root>/api-token`): the machine-local credential behind\n * `Authorization: Bearer`.\n *\n * Minted fresh at every server boot (same recipe as auth-session tokens) and written to\n * the data root with owner-only permissions, so anything that can read the file can call\n * the API as the admin. That equivalence is the authorization model, not an accident:\n * local filesystem access to the data root already IS admin authority — the same rule\n * `penguin server reset-admin-password` stands on (whoever can run it owns web.db\n * anyway) — and agents driving their own harness through the CLI is the product feature\n * this token exists for (server-driven Sessions hand it to tool subprocesses as\n * PENGUIN_API_TOKEN). Rotation per boot bounds the life of any leaked copy to the\n * process that minted it.\n */\nimport { createHash, randomBytes, timingSafeEqual } from \"node:crypto\";\nimport fs from \"node:fs\";\nimport path from \"node:path\";\n\nexport function apiTokenPath(root: string): string {\n return path.join(root, \"api-token\");\n}\n\n/** Mints a boot-scoped local API token (the auth-session token recipe). */\nexport function mintApiToken(): string {\n return randomBytes(32).toString(\"base64url\");\n}\n\n/**\n * Persists the boot token (owner-only file, tmp + rename so a concurrent reader never\n * sees a partial write). Best-effort like the initial-password file: an exotic read-only\n * root must not stop the server — local CLI callers then fall back to\n * PENGUIN_API_TOKEN.\n */\nexport function storeApiToken(root: string, token: string): void {\n try {\n fs.mkdirSync(root, { recursive: true });\n const target = apiTokenPath(root);\n const tmp = `${target}.${process.pid}.tmp`;\n fs.writeFileSync(tmp, `${token}\\n`, { mode: 0o600 });\n fs.renameSync(tmp, target);\n } catch {\n // Best-effort: Bearer auth still works for callers holding the token some other way.\n }\n}\n\n/** The stored token, or null when absent/unreadable/empty (shared with the CLI's file fallback). */\nexport function readApiToken(root: string): string | null {\n try {\n const value = fs.readFileSync(apiTokenPath(root), \"utf8\").trim();\n return value === \"\" ? null : value;\n } catch {\n return null;\n }\n}\n\n/**\n * Constant-time equality for token checks: both sides are reduced to fixed-size sha256\n * digests first, so the comparison's timing depends on neither the content nor the\n * length of an attacker-supplied value.\n */\nexport function tokensEqual(a: string, b: string): boolean {\n const digest = (v: string): Buffer => createHash(\"sha256\").update(v).digest();\n return timingSafeEqual(digest(a), digest(b));\n}\n"],"mappings":";;;;;;;;;;;;;;AAAA,SAAS,WAAW,iBAAiB;AAYrC,SAAS,aAAa,QAAQ,uBAAuB;AAS9C,IAAM,cAAc;AAC3B,IAAM,WAAW;AACjB,IAAM,WAAW;AACjB,IAAM,aAAa;AACnB,IAAM,YAAY;AAElB,SAAS,YACP,UACA,MACA,QACA,GACA,GACA,GACiB;AACjB,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,WAAO,UAAU,MAAM,QAAQ,EAAE,GAAG,GAAG,GAAG,GAAG,QAAQ,MAAM,OAAO,KAAK,GAAG,CAAC,KAAK,QAAQ;AACtF,UAAI,IAAK,QAAO,GAAG;AAAA,UACd,SAAQ,GAAG;AAAA,IAClB,CAAC;AAAA,EACH,CAAC;AACH;AAOA,eAAsB,aAAa,UAAkB,OAAe,aAA8B;AAChG,QAAM,OAAO,YAAY,UAAU;AACnC,QAAM,MAAM,MAAM,YAAY,UAAU,MAAM,WAAW,MAAM,UAAU,QAAQ;AACjF,SAAO;AAAA,IACL;AAAA,IACA,OAAO,IAAI;AAAA,IACX,OAAO,QAAQ;AAAA,IACf,OAAO,QAAQ;AAAA,IACf,KAAK,SAAS,QAAQ;AAAA,IACtB,IAAI,SAAS,QAAQ;AAAA,EACvB,EAAE,KAAK,GAAG;AACZ;AAUA,eAAsB,cAAc,UAAkB,QAAwC;AAC5F,QAAM,QAAQ,OAAO,MAAM,GAAG;AAC9B,MAAI,MAAM,WAAW,KAAK,MAAM,CAAC,MAAM,SAAU,QAAO;AACxD,QAAM,IAAI,OAAO,MAAM,CAAC,CAAC;AACzB,QAAM,IAAI,OAAO,MAAM,CAAC,CAAC;AACzB,QAAM,IAAI,OAAO,MAAM,CAAC,CAAC;AACzB,MAAI,CAAC,OAAO,UAAU,CAAC,KAAK,CAAC,OAAO,UAAU,CAAC,KAAK,CAAC,OAAO,UAAU,CAAC,EAAG,QAAO;AACjF,QAAM,OAAO,OAAO,KAAK,MAAM,CAAC,GAAI,QAAQ;AAC5C,QAAM,WAAW,OAAO,KAAK,MAAM,CAAC,GAAI,QAAQ;AAChD,MAAI,KAAK,WAAW,KAAK,SAAS,WAAW,EAAG,QAAO;AACvD,MAAI;AACJ,MAAI;AACF,aAAS,MAAM,YAAY,UAAU,MAAM,SAAS,QAAQ,GAAG,GAAG,CAAC;AAAA,EACrE,QAAQ;AACN,WAAO;AAAA,EACT;AACA,SAAO,gBAAgB,QAAQ,QAAQ,IAAI,UAAU;AACvD;AAGA,eAAsB,eAAe,UAAkB,QAAkC;AACvF,SAAQ,MAAM,cAAc,UAAU,MAAM,MAAO;AACrD;AAWA,eAAsB,sBACpB,UACA,QACA,WACA,QAAsE,eACpD;AAClB,QAAM,UAAU,WAAW,OAAO,iBAAiB,MAAM,MAAM,UAAU,MAAM;AAC/E,MAAI,YAAY,eAAgB,QAAO,YAAY;AACnD,QAAM,MAAM,UAAU,MAAM,UAAU,CAAC;AACvC,SAAO;AACT;AAhHA;AAmHA,8BAAC,UAAU;AACJ,IAAe,iBAAf,MAA8B;AAErC;AAFO;AAAe,iBAAf,8CADP,4BACsB;AAAf,4BAAe;AApHtB,8BAAAA;AAyHA,4BAAC,UAAU;AACJ,IAAM,eAAN,MAA6C;AAAA,EAClD,KAAK,UAAmC;AACtC,WAAO,aAAa,UAAU,WAAW;AAAA,EAC3C;AACF;AAJOA,SAAA;AAAM,eAAN,kBAAAA,QAAA,mBADP,0BACa;AAAN,kBAAAA,QAAA,GAAM;;;AClHb,SAAS,eAAAC,oBAAmB;;;ACM5B,SAAS,YAAY,eAAAC,cAAa,mBAAAC,wBAAuB;AACzD,OAAO,QAAQ;AACf,OAAO,UAAU;AAEV,SAAS,aAAa,MAAsB;AACjD,SAAO,KAAK,KAAK,MAAM,WAAW;AACpC;AAGO,SAAS,eAAuB;AACrC,SAAOD,aAAY,EAAE,EAAE,SAAS,WAAW;AAC7C;AAQO,SAAS,cAAc,MAAc,OAAqB;AAC/D,MAAI;AACF,OAAG,UAAU,MAAM,EAAE,WAAW,KAAK,CAAC;AACtC,UAAM,SAAS,aAAa,IAAI;AAChC,UAAM,MAAM,GAAG,MAAM,IAAI,QAAQ,GAAG;AACpC,OAAG,cAAc,KAAK,GAAG,KAAK;AAAA,GAAM,EAAE,MAAM,IAAM,CAAC;AACnD,OAAG,WAAW,KAAK,MAAM;AAAA,EAC3B,QAAQ;AAAA,EAER;AACF;AAiBO,SAAS,YAAY,GAAW,GAAoB;AACzD,QAAM,SAAS,CAAC,MAAsB,WAAW,QAAQ,EAAE,OAAO,CAAC,EAAE,OAAO;AAC5E,SAAOE,iBAAgB,OAAO,CAAC,GAAG,OAAO,CAAC,CAAC;AAC7C;;;AD7CA,SAAS,aAAAC,YAAW,KAAK,aAAAC,kBAAiB;AAInC,IAAM,sBAAsB;AAG5B,IAAM,gBAAgB;AAU7B,IAAM,sBAAsB;AAC5B,IAAM,yBAAyB;AAC/B,IAAM,uBAAuB;AAE7B,IAAM,wBAAwB,KAAK;AAGnC,IAAM,sBAAsB;AAMrB,SAAS,+BAAuC;AACrD,SAAOC,aAAY,mBAAmB,EAAE,SAAS,WAAW,EAAE,MAAM,GAAG,mBAAmB;AAC5F;AAUO,SAAS,WAAW,KAAwB;AACjD,SAAO;AAAA,IACL,QAAQ,IAAI;AAAA,IACZ,SAAS,IAAI;AAAA,IACb,mBAAmB,IAAI;AAAA,IACvB,GAAI,IAAI,gBAAgB,OAAO,EAAE,aAAa,IAAI,YAAY,IAAI,CAAC;AAAA,IACnE,GAAI,IAAI,WAAW,OAAO,EAAE,QAAQ,IAAI,OAAO,IAAI,CAAC;AAAA,IACpD,WAAW,IAAI;AAAA,EACjB;AACF;AArEA,gIAAAC;AAuEA,2BAACC,WAAU,IAET,cAAC,IAAI,IACL,qBAAC,IAAI,IAKL,cAAC,IAAI,IAEL,oBAAC,IAAI,IACL,eAAC,IAAI,IACL,cAAC,IAAI,IACL,eAAC,IAAI;AAZA,IAAM,cAAN,MAAkC;AAAA,EAAlC;AACE,wBAAiB,SAAjB,kBAAAD,QAAA,6BAAAA,QAAA;AACA,wBAAiB,gBAAjB,kBAAAA,QAAA,8BAAAA,QAAA;AAKA,wBAAiB,SAAjB,kBAAAA,QAAA,8BAAAA,QAAA;AAEA,wBAAiB,eAAjB,kBAAAA,QAAA,8BAAAA,QAAA;AACA,wBAAiB,UAAjB,kBAAAA,QAAA,8BAAAA,QAAA;AACA,wBAAiB,SAAjB,kBAAAA,QAAA,8BAAAA,QAAA;AACA,wBAAiB,UAAjB,kBAAAA,QAAA,8BAAAA,QAAA;AA8FP;AAAA,wBAAiB,iBAAgB,oBAAI,IAAyD;AAG9F;AAAA,wBAAQ,aAA2B;AAAA;AAAA;AAAA,EA/FnC,IAAI,eAAuB;AACzB,WAAO,KAAK,OAAO;AAAA,EACrB;AAAA,EACA,IAAY,iBAAyB;AACnC,WAAO,KAAK,OAAO;AAAA,EACrB;AAAA;AAAA,EAEA,IAAY,oBAAmC;AAC7C,WAAO,KAAK,OAAO;AAAA,EACrB;AAAA,EAEA,QAAc;AACZ,SAAK,aAAa,cAAc,KAAK,MAAM,IAAI,EAAE,YAAY,CAAC;AAAA,EAChE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,iBAAgC;AAC9B,QAAI,CAAC,KAAK,uBAAuB,EAAG,QAAO;AAC3C,QACE,KAAK,MAAM,oBAAoB,QAC/B,KAAK,qBAAqB,KAAK,MAAM,eAAe,MAAM,MAC1D;AACA,WAAK,MAAM,kBAAkB;AAAA,IAC/B;AACA,SAAK,MAAM,oBAAoB,KAAK,aAAa,eAAe,OAAO;AACvE,WAAO,KAAK,MAAM;AAAA,EACpB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,gBAAgB,UAAoC;AACxD,UAAM,MAAM,KAAK,MAAM,SAAS,aAAa;AAC7C,WAAO,QAAQ,QAAS,MAAM,eAAe,UAAU,IAAI,YAAY;AAAA,EACzE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,YAA2B;AAC/B,QAAI,KAAK,MAAM,MAAM,IAAI,EAAG;AAC5B,UAAM,WAAW,KAAK,qBAAqB,6BAA6B;AAGxE,QAAI,SAAS,SAAS,qBAAqB;AACzC,YAAM,IAAI;AAAA,QACR,gDAAgD,mBAAmB;AAAA,MACrE;AAAA,IACF;AACA,UAAM,OAAgB;AAAA,MACpB,QAAQ;AAAA,MACR,cAAc,MAAM,KAAK,OAAO,KAAK,QAAQ;AAAA,MAC7C,SAAS;AAAA,MACT,mBAAmB;AAAA,MACnB,aAAa;AAAA,MACb,QAAQ;AAAA,MACR,WAAW,KAAK,MAAM,IAAI,EAAE,YAAY;AAAA,IAC1C;AACA,SAAK,MAAM,OAAO,IAAI;AACtB,QAAI;AACF,YAAM,KAAK,YAAY,wBAAwB,MAAM,IAAI;AAAA,IAC3D,SAAS,KAAK;AACZ,WAAK,MAAM,OAAO,KAAK,MAAM;AAC7B,YAAM;AAAA,IACR;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,iBAAiB,OAA8B;AAC7C,UAAM,WAAW,KAAK,MAAM;AAC5B,QAAI,aAAa,QAAQ,UAAU,MAAM,CAAC,YAAY,OAAO,QAAQ,EAAG,QAAO;AAC/E,WAAO,KAAK,qBAAqB,QAAQ,MAAM,OAAO,OAAO;AAAA,EAC/D;AAAA;AAAA,EAGA,yBAAkC;AAChC,WAAO,KAAK,MAAM,SAAS,aAAa,GAAG,sBAAsB;AAAA,EACnE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAcA,MAAc,iBAAkC;AAC9C,SAAK,cAAc,MAAM,KAAK,OAAO,KAAKD,aAAY,EAAE,EAAE,SAAS,WAAW,CAAC;AAC/E,WAAO,KAAK;AAAA,EACd;AAAA;AAAA,EAGQ,aAAa,UAA0B;AAC7C,UAAM,SAAS,WAAW;AAC1B,QAAI,UAAU,EAAG,QAAO;AACxB,WAAO,KAAK,IAAI,yBAAyB,MAAM,SAAS,IAAI,oBAAoB;AAAA,EAClF;AAAA,EAEA,MAAM,MAAM,QAAgB,UAA8D;AACxF,UAAM,QAAQ,KAAK,MAAM,IAAI,EAAE,QAAQ;AACvC,eAAW,CAAC,KAAK,KAAK,KAAK,KAAK,eAAe;AAC7C,UAAI,QAAQ,MAAM,gBAAgB,sBAAuB,MAAK,cAAc,OAAO,GAAG;AAAA,IACxF;AACA,UAAM,SAAS,KAAK,cAAc,IAAI,MAAM;AAC5C,QAAI,QAAQ;AACV,YAAM,UAAU,OAAO,gBAAgB,KAAK,aAAa,OAAO,QAAQ;AACxE,UAAI,QAAQ,SAAS;AACnB,cAAM,IAAI;AAAA,UACR;AAAA,UACA;AAAA,UACA,kDAAkD,KAAK,MAAM,UAAU,SAAS,GAAI,CAAC;AAAA,QACvF;AAAA,MACF;AAAA,IACF;AACA,UAAM,MAAM,KAAK,MAAM,SAAS,MAAM;AAGtC,UAAM,KAAK,MAAM;AAAA,MAAsB;AAAA,MAAU,KAAK,gBAAgB;AAAA,MAAM,MAC1E,KAAK,eAAe;AAAA,IACtB;AACA,QAAI,CAAC,OAAO,CAAC,IAAI;AACf,WAAK,cAAc,IAAI,QAAQ;AAAA,QAC7B,WAAW,QAAQ,YAAY,KAAK;AAAA,QACpC,eAAe,KAAK,MAAM,IAAI,EAAE,QAAQ;AAAA,MAC1C,CAAC;AACD,YAAM,IAAI,UAAU,KAAK,uBAAuB,iCAAiC;AAAA,IACnF;AACA,SAAK,cAAc,OAAO,MAAM;AAChC,SAAK,aAAa,cAAc,KAAK,MAAM,IAAI,EAAE,YAAY,CAAC;AAC9D,WAAO,EAAE,MAAM,WAAW,GAAG,GAAG,OAAO,KAAK,aAAa,IAAI,QAAQ,UAAU,EAAE;AAAA,EACnF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,QAAQ,QAAyB;AAC/B,WAAO,KAAK,MAAM,SAAS,MAAM,GAAG,YAAY;AAAA,EAClD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,eAAkD;AAChD,UAAM,MAAM,KAAK,MAAM,SAAS,aAAa;AAC7C,QAAI,CAAC,KAAK;AACR,YAAM,IAAI,UAAU,KAAK,YAAY,qCAAqC;AAAA,IAC5E;AACA,WAAO,EAAE,MAAM,WAAW,GAAG,GAAG,OAAO,KAAK,aAAa,IAAI,QAAQ,SAAS,EAAE;AAAA,EAClF;AAAA;AAAA,EAGA,MAAM,eAAe,QAAgB,aAAqB,aAAoC;AAC5F,UAAM,MAAM,KAAK,MAAM,SAAS,MAAM;AACtC,QAAI,CAAC,OAAO,CAAE,MAAM,eAAe,aAAa,IAAI,YAAY,GAAI;AAClE,YAAM,IAAI,UAAU,KAAK,qBAAqB,gCAAgC;AAAA,IAChF;AACA,UAAM,KAAK,YAAY,QAAQ,WAAW;AAAA,EAC5C;AAAA;AAAA,EAGA,MAAM,mBAAmB,QAAgB,aAAoC;AAC3E,UAAM,KAAK,YAAY,QAAQ,WAAW;AAAA,EAC5C;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAc,YAAY,QAAgB,aAAoC;AAC5E,QAAI,YAAY,SAAS,qBAAqB;AAC5C,YAAM,IAAI,UAAU,KAAK,oBAAoB,yCAAyC;AAAA,IACxF;AACA,SAAK,MAAM,eAAe,QAAQ,MAAM,KAAK,OAAO,KAAK,WAAW,GAAG,KAAK;AAM5E,SAAK,cAAc,OAAO,MAAM;AAKhC,QAAI,WAAW,eAAe;AAC5B,WAAK,aAAa,mBAAmB,QAAQ,OAAO;AACpD,WAAK,MAAM,kBAAkB;AAAA,IAC/B;AAAA,EACF;AAAA;AAAA,EAGA,OAAO,OAAqB;AAC1B,SAAK,aAAa,OAAO,iBAAiB,KAAK,CAAC;AAAA,EAClD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,gBAA+B;AAC7B,WAAO,KAAK,MAAM;AAAA,EACpB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,qBAAqB,OAA0D;AAC7E,UAAM,WAAW,KAAK,MAAM;AAC5B,QAAI,aAAa,QAAQ,MAAM,WAAW,EAAG,QAAO;AACpD,QAAI,CAAC,YAAY,OAAO,QAAQ,EAAG,QAAO;AAC1C,UAAM,OAAO,KAAK,MAAM,SAAS,aAAa;AAC9C,WAAO,SAAS,OAAO,OAAO,EAAE,MAAM,KAAK,QAAQ;AAAA,EACrD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,qBAAqB,OAA4E;AAC/F,UAAM,YAAY,iBAAiB,KAAK;AACxC,UAAM,UAAU,KAAK,aAAa,gBAAgB,SAAS;AAC3D,QAAI,CAAC,QAAS,QAAO;AACrB,UAAM,MAAM,KAAK,MAAM,IAAI;AAC3B,UAAM,YAAY,KAAK,MAAM,QAAQ,SAAS;AAC9C,QAAI,aAAa,IAAI,QAAQ,GAAG;AAC9B,WAAK,aAAa,OAAO,SAAS;AAClC,aAAO;AAAA,IACT;AACA,UAAM,OAAO,KAAK,MAAM,SAAS,QAAQ,MAAM;AAC/C,QAAI,CAAC,KAAM,QAAO;AAClB,UAAM,YAAY,YAAY,KAAK,MAAM,QAAQ,SAAS,KAAK,KAAK;AACpE,QAAI,UAAU;AACd,QAAI,aAAa,YAAY,IAAI,QAAQ,IAAI,KAAK,gBAAgB;AAChE,WAAK,aAAa,MAAM,WAAW,IAAI,KAAK,IAAI,QAAQ,IAAI,KAAK,YAAY,EAAE,YAAY,CAAC;AAC5F,gBAAU;AAAA,IACZ;AACA,UAAM,MACJ,QAAQ,QAAQ,YAAY,YAAY,QAAQ,QAAQ,UAAU,UAAU;AAC9E,WAAO,EAAE,MAAM,KAAK,QAAQ;AAAA,EAC9B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,cAAc,OAAwB;AACpC,UAAM,UAAU,KAAK,aAAa,gBAAgB,iBAAiB,KAAK,CAAC;AACzE,QAAI,CAAC,QAAS,QAAO;AACrB,WAAO,KAAK,MAAM,QAAQ,SAAS,IAAI,KAAK,MAAM,IAAI,EAAE,QAAQ;AAAA,EAClE;AAAA,EAEQ,aAAa,QAAgB,KAA8B;AACjE,WAAO,KAAK,aAAa,MAAM;AAAA,MAC7B;AAAA,MACA;AAAA,MACA,KAAK,KAAK,MAAM,IAAI;AAAA,MACpB,OAAO,KAAK;AAAA,IACd,CAAC,EAAE;AAAA,EACL;AACF;AAlTOC,SAAA;AACE,kBAAAA,QAAA,GAAiB,SAAxB,YADW;AAEJ,kBAAAA,QAAA,GAAiB,gBAAxB,mBAFW;AAOJ,kBAAAA,QAAA,GAAiB,SAAxB,YAPW;AASJ,kBAAAA,QAAA,GAAiB,eAAxB,kBATW;AAUJ,kBAAAA,QAAA,GAAiB,UAAxB,aAVW;AAWJ,kBAAAA,QAAA,GAAiB,SAAxB,YAXW;AAYJ,kBAAAA,QAAA,GAAiB,UAAxB,aAZW;AAAA,cAAN,kBAAAA,QAAA,kBADP,yBACa;AAAN,kBAAAA,QAAA,GAAM;AAxEb,2CAAAA;AA6XA,yCAACE,WAAU;AACJ,IAAe,4BAAf,MAAyC;AAEhD;AAFOF,SAAA;AAAe,4BAAf,kBAAAA,QAAA,gCADP,uCACsB;AAAf,kBAAAA,QAAA,GAAe;","names":["_init","randomBytes","randomBytes","timingSafeEqual","timingSafeEqual","Component","Interface","randomBytes","_init","Component","Interface"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/db/repos/machines.ts"],"sourcesContent":["/**\n * What this server remembers about machines: its own identity, one row per machine it has\n * installed on or reached (what is installed there, the session held to it), and which\n * machines each Project uses. All in web.db, so a hot swap or a restart reads back exactly\n * what the last generation wrote — the JSON file this replaces could not survive a schema\n * change, and nothing else this server remembers lives outside the database.\n */\nimport type { DatabaseSync } from \"node:sqlite\";\nimport { randomBytes } from \"node:crypto\";\n\nexport interface MachineRow {\n /** `ssh:<alias>` */\n address: string;\n /** That machine's own id, once heard. */\n machineId: string | null;\n /** What this server last installed there; null when it never has. */\n version: string | null;\n installedAt: string | null;\n /**\n * Non-null while a connection to it is HELD: the pid of the session as of the last connect.\n * A record of intent, not a handle — a restart or a hot push re-holds every machine with one,\n * and a disconnect clears it. It is never used to kill anything: a pid read back from a file\n * may by then be anyone's.\n */\n sessionPid: number | null;\n /** The port its server was bound to over there, as of the last connect. */\n remotePort: number | null;\n /** What the install found the machine to be; null until one has. The status probe speaks that dialect. */\n platform: \"linux\" | \"darwin\" | \"win32\" | null;\n}\n\n/** Everything but the address may be patched; absent fields keep their value. */\ntype MachinePatch = Partial<Omit<MachineRow, \"address\">>;\n\n/**\n * 12 random bytes as base64url: 16 characters a person can read in a tooltip, 96 bits\n * against ids minted on machines that never coordinate.\n */\nconst MACHINE_ID_BYTES = 12;\n\nexport class MachinesRepo {\n constructor(private readonly db: DatabaseSync) {}\n\n /**\n * This server's own id if one has been minted, WITHOUT minting one — `penguin server\n * status` runs on a data root whose server may never have started, and must not create an\n * identity as a side effect of the question.\n */\n peekOwnId(): string | null {\n const row = this.db.prepare(\"SELECT machine_id FROM machine WHERE singleton = 1\").get();\n return row ? (row.machine_id as string) : null;\n }\n\n /** This server's own id, minted on first call and stable ever after. */\n ownId(): string {\n const existing = this.peekOwnId();\n if (existing !== null) return existing;\n const minted = randomBytes(MACHINE_ID_BYTES).toString(\"base64url\");\n this.db.prepare(\"INSERT INTO machine (singleton, machine_id) VALUES (1, ?)\").run(minted);\n return minted;\n }\n\n get(address: string): MachineRow | null {\n const row = this.db.prepare(\"SELECT * FROM machines WHERE address = ?\").get(address);\n return row === undefined ? null : toRow(row);\n }\n\n /**\n * Every row answering to a machine's own id — two aliases for one host are two rows with\n * one id. Newest install first, then by address, so the order is the same every time; which\n * of them to speak through is the service's to decide (it knows which has a session).\n */\n byMachineId(machineId: string): MachineRow[] {\n return this.db\n .prepare(\n \"SELECT * FROM machines WHERE machine_id = ? ORDER BY installed_at DESC, address ASC\",\n )\n .all(machineId)\n .map(toRow);\n }\n\n all(): MachineRow[] {\n return this.db.prepare(\"SELECT * FROM machines\").all().map(toRow);\n }\n\n /** Writes the fields given, creating the row when there is none. */\n patch(address: string, patch: MachinePatch): void {\n const next: MachineRow = {\n ...(this.get(address) ?? {\n address,\n machineId: null,\n version: null,\n installedAt: null,\n sessionPid: null,\n remotePort: null,\n platform: null,\n }),\n ...patch,\n };\n this.db\n .prepare(\n \"INSERT OR REPLACE INTO machines (address, machine_id, version, installed_at, session_pid, remote_port, platform) VALUES (?, ?, ?, ?, ?, ?, ?)\",\n )\n .run(\n address,\n next.machineId,\n next.version,\n next.installedAt,\n next.sessionPid,\n next.remotePort,\n next.platform,\n );\n }\n\n /** The addresses a Project uses, or null when it has never had a list written for it. */\n members(projectId: string): string[] | null {\n const row = this.db\n .prepare(\"SELECT addresses FROM machine_project WHERE project_id = ?\")\n .get(projectId);\n return row ? (JSON.parse(row.addresses as string) as string[]) : null;\n }\n\n setMembers(projectId: string, addresses: string[]): void {\n this.db\n .prepare(\"INSERT OR REPLACE INTO machine_project (project_id, addresses) VALUES (?, ?)\")\n .run(projectId, JSON.stringify(addresses));\n }\n}\n\nfunction toRow(row: Record<string, unknown>): MachineRow {\n return {\n address: row.address as string,\n machineId: (row.machine_id as string | null) ?? null,\n version: (row.version as string | null) ?? null,\n installedAt: (row.installed_at as string | null) ?? null,\n sessionPid: (row.session_pid as number | null) ?? null,\n remotePort: (row.remote_port as number | null) ?? null,\n platform: (row.platform as MachineRow[\"platform\"]) ?? null,\n };\n}\n"],"mappings":";AAQA,SAAS,mBAAmB;AA8B5B,IAAM,mBAAmB;AAElB,IAAM,eAAN,MAAmB;AAAA,EACxB,YAA6B,IAAkB;AAAlB;AAAA,EAAmB;AAAA,EAAnB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAO7B,YAA2B;AACzB,UAAM,MAAM,KAAK,GAAG,QAAQ,oDAAoD,EAAE,IAAI;AACtF,WAAO,MAAO,IAAI,aAAwB;AAAA,EAC5C;AAAA;AAAA,EAGA,QAAgB;AACd,UAAM,WAAW,KAAK,UAAU;AAChC,QAAI,aAAa,KAAM,QAAO;AAC9B,UAAM,SAAS,YAAY,gBAAgB,EAAE,SAAS,WAAW;AACjE,SAAK,GAAG,QAAQ,2DAA2D,EAAE,IAAI,MAAM;AACvF,WAAO;AAAA,EACT;AAAA,EAEA,IAAI,SAAoC;AACtC,UAAM,MAAM,KAAK,GAAG,QAAQ,0CAA0C,EAAE,IAAI,OAAO;AACnF,WAAO,QAAQ,SAAY,OAAO,MAAM,GAAG;AAAA,EAC7C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,YAAY,WAAiC;AAC3C,WAAO,KAAK,GACT;AAAA,MACC;AAAA,IACF,EACC,IAAI,SAAS,EACb,IAAI,KAAK;AAAA,EACd;AAAA,EAEA,MAAoB;AAClB,WAAO,KAAK,GAAG,QAAQ,wBAAwB,EAAE,IAAI,EAAE,IAAI,KAAK;AAAA,EAClE;AAAA;AAAA,EAGA,MAAM,SAAiB,OAA2B;AAChD,UAAM,OAAmB;AAAA,MACvB,GAAI,KAAK,IAAI,OAAO,KAAK;AAAA,QACvB;AAAA,QACA,WAAW;AAAA,QACX,SAAS;AAAA,QACT,aAAa;AAAA,QACb,YAAY;AAAA,QACZ,YAAY;AAAA,QACZ,UAAU;AAAA,MACZ;AAAA,MACA,GAAG;AAAA,IACL;AACA,SAAK,GACF;AAAA,MACC;AAAA,IACF,EACC;AAAA,MACC;AAAA,MACA,KAAK;AAAA,MACL,KAAK;AAAA,MACL,KAAK;AAAA,MACL,KAAK;AAAA,MACL,KAAK;AAAA,MACL,KAAK;AAAA,IACP;AAAA,EACJ;AAAA;AAAA,EAGA,QAAQ,WAAoC;AAC1C,UAAM,MAAM,KAAK,GACd,QAAQ,4DAA4D,EACpE,IAAI,SAAS;AAChB,WAAO,MAAO,KAAK,MAAM,IAAI,SAAmB,IAAiB;AAAA,EACnE;AAAA,EAEA,WAAW,WAAmB,WAA2B;AACvD,SAAK,GACF,QAAQ,8EAA8E,EACtF,IAAI,WAAW,KAAK,UAAU,SAAS,CAAC;AAAA,EAC7C;AACF;AAEA,SAAS,MAAM,KAA0C;AACvD,SAAO;AAAA,IACL,SAAS,IAAI;AAAA,IACb,WAAY,IAAI,cAAgC;AAAA,IAChD,SAAU,IAAI,WAA6B;AAAA,IAC3C,aAAc,IAAI,gBAAkC;AAAA,IACpD,YAAa,IAAI,eAAiC;AAAA,IAClD,YAAa,IAAI,eAAiC;AAAA,IAClD,UAAW,IAAI,YAAuC;AAAA,EACxD;AACF;","names":[]}
|
package/dist/chunk-JKJXQTHH.js
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
// src/api/types.ts
|
|
2
|
-
var MACHINE_PHASES = [
|
|
3
|
-
"build",
|
|
4
|
-
"check",
|
|
5
|
-
"install",
|
|
6
|
-
"handover",
|
|
7
|
-
"restart",
|
|
8
|
-
"connect",
|
|
9
|
-
"sync",
|
|
10
|
-
"stop",
|
|
11
|
-
"remove"
|
|
12
|
-
];
|
|
13
|
-
var TICKET_SLUG_PATTERN = /^[a-z]+(?:-[a-z]+)*$/;
|
|
14
|
-
|
|
15
|
-
export {
|
|
16
|
-
MACHINE_PHASES,
|
|
17
|
-
TICKET_SLUG_PATTERN
|
|
18
|
-
};
|
|
19
|
-
//# sourceMappingURL=chunk-JKJXQTHH.js.map
|