clay-server 2.16.0 → 2.17.0-beta.1

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/bin/cli.js CHANGED
@@ -52,6 +52,7 @@ function openUrl(url) {
52
52
  var args = process.argv.slice(2);
53
53
  var port = _isDev ? 2635 : 2633;
54
54
  var useHttps = true;
55
+ var forceMkcert = false;
55
56
  var skipUpdate = false;
56
57
  var debugMode = false;
57
58
  var autoYes = false;
@@ -81,6 +82,8 @@ for (var i = 0; i < args.length; i++) {
81
82
  i++;
82
83
  } else if (args[i] === "--no-https") {
83
84
  useHttps = false;
85
+ } else if (args[i] === "--local-cert") {
86
+ forceMkcert = true;
84
87
  } else if (args[i] === "--no-update" || args[i] === "--skip-update") {
85
88
  skipUpdate = true;
86
89
  } else if (args[i] === "--dev") {
@@ -124,7 +127,8 @@ for (var i = 0; i < args.length; i++) {
124
127
  console.log("Options:");
125
128
  console.log(" -p, --port <port> Port to listen on (default: 2633)");
126
129
  console.log(" --host <address> Address to bind to (default: 0.0.0.0)");
127
- console.log(" --no-https Disable HTTPS (enabled by default via mkcert)");
130
+ console.log(" --no-https Disable HTTPS (enabled by default)");
131
+ console.log(" --local-cert Use local certificate (mkcert) instead of builtin");
128
132
  console.log(" --no-update Skip auto-update check on startup");
129
133
  console.log(" --debug Enable debug panel in the web UI");
130
134
  console.log(" -y, --yes Skip interactive prompts (accept defaults)");
@@ -564,7 +568,43 @@ function getAllIPs() {
564
568
  return ips;
565
569
  }
566
570
 
571
+ function getBuiltinCert() {
572
+ try {
573
+ var certDir = path.join(__dirname, "..", "lib", "certs");
574
+ var keyPath = path.join(certDir, "privkey.pem");
575
+ var certPath = path.join(certDir, "fullchain.pem");
576
+ if (!fs.existsSync(keyPath) || !fs.existsSync(certPath)) return null;
577
+
578
+ // Check expiry
579
+ var certText = execFileSync("openssl", [
580
+ "x509", "-in", certPath, "-noout", "-enddate"
581
+ ], { encoding: "utf8" });
582
+ var m = certText.match(/notAfter=(.+)/);
583
+ if (m) {
584
+ var expiry = new Date(m[1]);
585
+ var now = new Date();
586
+ // Skip if expiring within 7 days
587
+ if (expiry.getTime() - now.getTime() < 7 * 24 * 60 * 60 * 1000) return null;
588
+ }
589
+
590
+ return { key: keyPath, cert: certPath, caRoot: null, builtin: true };
591
+ } catch (e) {
592
+ return null;
593
+ }
594
+ }
595
+
596
+ function toClayStudioUrl(ip, port, protocol) {
597
+ var dashed = ip.replace(/\./g, "-");
598
+ return protocol + "://" + dashed + ".d.clay.studio:" + port;
599
+ }
600
+
567
601
  function ensureCerts(ip) {
602
+ // Check builtin cert first (unless --local-cert flag is set)
603
+ if (!forceMkcert) {
604
+ var builtin = getBuiltinCert();
605
+ if (builtin) return builtin;
606
+ }
607
+
568
608
  var homeDir = os.homedir();
569
609
  var certDir = path.join(process.env.CLAY_HOME || path.join(homeDir, ".clay"), "certs");
570
610
  var keyPath = path.join(certDir, "key.pem");
@@ -1395,11 +1435,13 @@ function setup(callback) {
1395
1435
  async function forkDaemon(mode, keepAwake, extraProjects, addCwd, wantOsUsers) {
1396
1436
  var ip = getLocalIP();
1397
1437
  var hasTls = false;
1438
+ var hasBuiltinCert = false;
1398
1439
 
1399
1440
  if (useHttps) {
1400
1441
  var certPaths = ensureCerts(ip);
1401
1442
  if (certPaths) {
1402
1443
  hasTls = true;
1444
+ if (certPaths.builtin) hasBuiltinCert = true;
1403
1445
  } else {
1404
1446
  log(sym.warn + " " + a.yellow + "HTTPS unavailable" + a.reset + a.dim + " · mkcert not installed" + a.reset);
1405
1447
  }
@@ -1473,6 +1515,7 @@ async function forkDaemon(mode, keepAwake, extraProjects, addCwd, wantOsUsers) {
1473
1515
  host: host,
1474
1516
  pinHash: mode === "multi" && cliPin ? generateAuthToken(cliPin) : null,
1475
1517
  tls: hasTls,
1518
+ builtinCert: hasBuiltinCert,
1476
1519
  debug: debugMode,
1477
1520
  keepAwake: keepAwake,
1478
1521
  dangerouslySkipPermissions: dangerouslySkipPermissions,
@@ -1547,7 +1590,9 @@ async function forkDaemon(mode, keepAwake, extraProjects, addCwd, wantOsUsers) {
1547
1590
  // Headless mode — print status and exit immediately
1548
1591
  if (headlessMode) {
1549
1592
  var protocol = config.tls ? "https" : "http";
1550
- var url = protocol + "://" + ip + ":" + config.port;
1593
+ var url = config.builtinCert
1594
+ ? toClayStudioUrl(ip, config.port, protocol)
1595
+ : protocol + "://" + ip + ":" + config.port;
1551
1596
  console.log(" " + sym.done + " Daemon started (PID " + config.pid + ")");
1552
1597
  console.log(" " + sym.done + " " + url);
1553
1598
  console.log(" " + sym.done + " Headless mode — exiting CLI");
@@ -1565,10 +1610,14 @@ async function forkDaemon(mode, keepAwake, extraProjects, addCwd, wantOsUsers) {
1565
1610
  async function devMode(mode, keepAwake, existingPinHash) {
1566
1611
  var ip = getLocalIP();
1567
1612
  var hasTls = false;
1613
+ var hasBuiltinCert = false;
1568
1614
 
1569
1615
  if (useHttps) {
1570
1616
  var certPaths = ensureCerts(ip);
1571
- if (certPaths) hasTls = true;
1617
+ if (certPaths) {
1618
+ hasTls = true;
1619
+ if (certPaths.builtin) hasBuiltinCert = true;
1620
+ }
1572
1621
  }
1573
1622
 
1574
1623
  var portFree = await isPortFree(port);
@@ -1630,6 +1679,7 @@ async function devMode(mode, keepAwake, existingPinHash) {
1630
1679
  host: host,
1631
1680
  pinHash: existingPinHash || null,
1632
1681
  tls: hasTls,
1682
+ builtinCert: hasBuiltinCert,
1633
1683
  debug: true,
1634
1684
  keepAwake: keepAwake || false,
1635
1685
  dangerouslySkipPermissions: dangerouslySkipPermissions,
@@ -1778,6 +1828,7 @@ async function restartDaemonWithTLS(config, callback) {
1778
1828
  callback(config);
1779
1829
  return;
1780
1830
  }
1831
+ var hasBuiltinCert = !!(certPaths && certPaths.builtin);
1781
1832
 
1782
1833
  // Shut down old daemon
1783
1834
  stopDaemonWatcher();
@@ -1801,6 +1852,7 @@ async function restartDaemonWithTLS(config, callback) {
1801
1852
  port: config.port,
1802
1853
  pinHash: config.pinHash || null,
1803
1854
  tls: true,
1855
+ builtinCert: hasBuiltinCert,
1804
1856
  debug: config.debug || false,
1805
1857
  keepAwake: config.keepAwake || false,
1806
1858
  dangerouslySkipPermissions: config.dangerouslySkipPermissions || false,
@@ -1879,7 +1931,9 @@ function showServerStarted(config, ip) {
1879
1931
  function showMainMenu(config, ip) {
1880
1932
  startDaemonWatcher();
1881
1933
  var protocol = config.tls ? "https" : "http";
1882
- var url = protocol + "://" + ip + ":" + config.port;
1934
+ var url = config.builtinCert
1935
+ ? toClayStudioUrl(ip, config.port, protocol)
1936
+ : protocol + "://" + ip + ":" + config.port;
1883
1937
 
1884
1938
  sendIPCCommand(socketPath(), { cmd: "get_status" }).then(function (status) {
1885
1939
  var projs = (status && status.projects) || [];
@@ -1925,7 +1979,6 @@ function showMainMenu(config, ip) {
1925
1979
  function showMenuItems() {
1926
1980
  var items = [
1927
1981
  { label: "Setup notifications", value: "notifications" },
1928
- { label: "Projects", value: "projects" },
1929
1982
  { label: "Settings", value: "settings" },
1930
1983
  { label: "Shut down server", value: "shutdown" },
1931
1984
  { label: "Keep server alive & exit", value: "exit" },
@@ -1940,10 +1993,6 @@ function showMainMenu(config, ip) {
1940
1993
  });
1941
1994
  break;
1942
1995
 
1943
- case "projects":
1944
- showProjectsMenu(config, ip);
1945
- break;
1946
-
1947
1996
  case "settings":
1948
1997
  showSettingsMenu(config, ip);
1949
1998
  break;
@@ -2000,203 +2049,6 @@ function showMainMenu(config, ip) {
2000
2049
  });
2001
2050
  }
2002
2051
 
2003
- // ==============================
2004
- // Projects sub-menu
2005
- // ==============================
2006
- function showProjectsMenu(config, ip) {
2007
- sendIPCCommand(socketPath(), { cmd: "get_status" }).then(function (status) {
2008
- if (!status.ok) {
2009
- log(a.red + "Failed to get status" + a.reset);
2010
- showMainMenu(config, ip);
2011
- return;
2012
- }
2013
-
2014
- console.clear();
2015
- printLogo();
2016
- log("");
2017
- log(sym.pointer + " " + a.bold + "Projects" + a.reset);
2018
- log(sym.bar);
2019
-
2020
- var projs = status.projects || [];
2021
- for (var i = 0; i < projs.length; i++) {
2022
- var p = projs[i];
2023
- var statusIcon = p.isProcessing ? "⚡" : (p.clients > 0 ? "🟢" : "⏸");
2024
- var sessionLabel = p.sessions === 1 ? "1 session" : p.sessions + " sessions";
2025
- var projName = p.title || p.project;
2026
- log(sym.bar + " " + a.bold + projName + a.reset + " " + sessionLabel + " " + statusIcon);
2027
- log(sym.bar + " " + a.dim + p.path + a.reset);
2028
- if (i < projs.length - 1) log(sym.bar);
2029
- }
2030
- log(sym.bar);
2031
-
2032
- // Build menu items
2033
- var items = [];
2034
-
2035
- // Check if cwd is already registered
2036
- var cwdRegistered = false;
2037
- for (var j = 0; j < projs.length; j++) {
2038
- if (projs[j].path === cwd) {
2039
- cwdRegistered = true;
2040
- break;
2041
- }
2042
- }
2043
- if (!cwdRegistered) {
2044
- items.push({ label: "+ Add " + a.bold + path.basename(cwd) + a.reset + " " + a.dim + "(" + cwd + ")" + a.reset, value: "add_cwd" });
2045
- }
2046
- items.push({ label: "+ Add project...", value: "add_other" });
2047
-
2048
- for (var k = 0; k < projs.length; k++) {
2049
- var itemLabel = projs[k].title || projs[k].project;
2050
- items.push({ label: itemLabel, value: "detail:" + projs[k].slug });
2051
- }
2052
- items.push({ label: "Back", value: "back" });
2053
-
2054
- promptSelect("Select", items, function (choice) {
2055
- if (choice === "back") {
2056
- console.clear();
2057
- printLogo();
2058
- log("");
2059
- showMainMenu(config, ip);
2060
- } else if (choice === "add_cwd") {
2061
- sendIPCCommand(socketPath(), { cmd: "add_project", path: cwd }).then(function (res) {
2062
- if (res.ok) {
2063
- log(sym.done + " " + a.green + "Added: " + res.slug + a.reset);
2064
- config = loadConfig() || config;
2065
- } else {
2066
- log(sym.warn + " " + a.yellow + (res.error || "Failed") + a.reset);
2067
- }
2068
- log("");
2069
- showProjectsMenu(config, ip);
2070
- });
2071
- } else if (choice === "add_other") {
2072
- log(sym.bar);
2073
- promptText("Directory path", cwd, function (dirPath) {
2074
- if (dirPath === null) {
2075
- showProjectsMenu(config, ip);
2076
- return;
2077
- }
2078
- var absPath = path.resolve(dirPath);
2079
- try {
2080
- var stat = fs.statSync(absPath);
2081
- if (!stat.isDirectory()) {
2082
- log(sym.warn + " " + a.red + "Not a directory: " + absPath + a.reset);
2083
- setTimeout(function () { showProjectsMenu(config, ip); }, 2000);
2084
- return;
2085
- }
2086
- } catch (e) {
2087
- log(sym.warn + " " + a.red + "Directory not found: " + absPath + a.reset);
2088
- setTimeout(function () { showProjectsMenu(config, ip); }, 2000);
2089
- return;
2090
- }
2091
- var alreadyExists = false;
2092
- for (var pi = 0; pi < projs.length; pi++) {
2093
- if (projs[pi].path === absPath) {
2094
- alreadyExists = true;
2095
- break;
2096
- }
2097
- }
2098
- if (alreadyExists) {
2099
- log(sym.done + " " + a.yellow + "Already added: " + path.basename(absPath) + a.reset + " " + a.dim + "(" + absPath + ")" + a.reset);
2100
- setTimeout(function () { showProjectsMenu(config, ip); }, 2000);
2101
- return;
2102
- }
2103
- sendIPCCommand(socketPath(), { cmd: "add_project", path: absPath }).then(function (res) {
2104
- if (res.ok) {
2105
- log(sym.done + " " + a.green + "Added: " + res.slug + a.reset + " " + a.dim + "(" + absPath + ")" + a.reset);
2106
- config = loadConfig() || config;
2107
- } else {
2108
- log(sym.warn + " " + a.yellow + (res.error || "Failed") + a.reset);
2109
- }
2110
- setTimeout(function () { showProjectsMenu(config, ip); }, 2000);
2111
- });
2112
- });
2113
- } else if (choice.startsWith("detail:")) {
2114
- var detailSlug = choice.substring(7);
2115
- showProjectDetail(config, ip, detailSlug, projs);
2116
- }
2117
- });
2118
- });
2119
- }
2120
-
2121
- // ==============================
2122
- // Project detail
2123
- // ==============================
2124
- function showProjectDetail(config, ip, slug, projects) {
2125
- var proj = null;
2126
- for (var i = 0; i < projects.length; i++) {
2127
- if (projects[i].slug === slug) {
2128
- proj = projects[i];
2129
- break;
2130
- }
2131
- }
2132
- if (!proj) {
2133
- showProjectsMenu(config, ip);
2134
- return;
2135
- }
2136
-
2137
- var displayName = proj.title || proj.project;
2138
-
2139
- console.clear();
2140
- printLogo();
2141
- log("");
2142
- log(sym.pointer + " " + a.bold + displayName + a.reset + " " + a.dim + proj.slug + " · " + proj.path + a.reset);
2143
- log(sym.bar);
2144
- var sessionLabel = proj.sessions === 1 ? "1 session" : proj.sessions + " sessions";
2145
- var clientLabel = proj.clients === 1 ? "1 client" : proj.clients + " clients";
2146
- log(sym.bar + " " + sessionLabel + " · " + clientLabel);
2147
- if (proj.title) {
2148
- log(sym.bar + " " + a.dim + "Title: " + a.reset + proj.title);
2149
- }
2150
- log(sym.bar);
2151
-
2152
- var items = [
2153
- { label: proj.title ? "Change title" : "Set title", value: "title" },
2154
- { label: "Remove project", value: "remove" },
2155
- { label: "Back", value: "back" },
2156
- ];
2157
-
2158
- promptSelect("What would you like to do?", items, function (choice) {
2159
- if (choice === "title") {
2160
- log(sym.bar);
2161
- promptText("Project title", proj.title || proj.project, function (newTitle) {
2162
- if (newTitle === null) {
2163
- showProjectDetail(config, ip, slug, projects);
2164
- return;
2165
- }
2166
- var titleVal = newTitle.trim();
2167
- // If same as directory name, clear custom title
2168
- if (titleVal === proj.project || titleVal === "") {
2169
- titleVal = null;
2170
- }
2171
- sendIPCCommand(socketPath(), { cmd: "set_project_title", slug: slug, title: titleVal }).then(function (res) {
2172
- if (res.ok) {
2173
- proj.title = titleVal;
2174
- config = loadConfig() || config;
2175
- log(sym.done + " " + a.green + "Title updated" + a.reset);
2176
- } else {
2177
- log(sym.warn + " " + a.yellow + (res.error || "Failed") + a.reset);
2178
- }
2179
- log("");
2180
- showProjectDetail(config, ip, slug, projects);
2181
- });
2182
- });
2183
- } else if (choice === "remove") {
2184
- sendIPCCommand(socketPath(), { cmd: "remove_project", slug: slug }).then(function (res) {
2185
- if (res.ok) {
2186
- log(sym.done + " " + a.green + "Removed: " + slug + a.reset);
2187
- config = loadConfig() || config;
2188
- } else {
2189
- log(sym.warn + " " + a.yellow + (res.error || "Failed") + a.reset);
2190
- }
2191
- log("");
2192
- showProjectsMenu(config, ip);
2193
- });
2194
- } else {
2195
- showProjectsMenu(config, ip);
2196
- }
2197
- });
2198
- }
2199
-
2200
2052
  // ==============================
2201
2053
  // Setup guide (2x2 toggle flow)
2202
2054
  // ==============================
@@ -2229,7 +2081,7 @@ function showSetupGuide(config, ip, goBack) {
2229
2081
  promptToggle("Access from outside your network?", "Requires Tailscale on both devices", false, function (remote) {
2230
2082
  wantRemote = remote;
2231
2083
  log(sym.bar);
2232
- promptToggle("Want push notifications?", "Requires HTTPS (mkcert certificate)", false, function (push) {
2084
+ promptToggle("Want push notifications?", "Requires HTTPS", false, function (push) {
2233
2085
  wantPush = push;
2234
2086
  log(sym.bar);
2235
2087
  afterToggles();
@@ -2293,6 +2145,15 @@ function showSetupGuide(config, ip, goBack) {
2293
2145
  return;
2294
2146
  }
2295
2147
 
2148
+ // Builtin cert: HTTPS already active, skip mkcert flow entirely
2149
+ if (config.builtinCert) {
2150
+ log(sym.pointer + " " + a.bold + "HTTPS" + a.reset + a.dim + " · Enabled (builtin certificate)" + a.reset);
2151
+ log(sym.bar);
2152
+ showSetupQR();
2153
+ return;
2154
+ }
2155
+
2156
+ // mkcert flow (--mkcert or fallback)
2296
2157
  var mcReady = hasMkcert();
2297
2158
  log(sym.pointer + " " + a.bold + "HTTPS Setup (for push notifications)" + a.reset);
2298
2159
  if (mcReady) {
@@ -2341,10 +2202,16 @@ function showSetupGuide(config, ip, goBack) {
2341
2202
  }
2342
2203
  var setupIP = wantRemote ? (tsIP || ip) : (lanIP || ip);
2343
2204
  var setupQuery = wantRemote ? "" : "?mode=lan";
2344
- // Always use HTTP onboarding URL for QR/setup when TLS is active
2345
- var setupUrl = config.tls
2346
- ? "http://" + setupIP + ":" + (config.port + 1) + "/setup" + setupQuery
2347
- : "http://" + setupIP + ":" + config.port + "/setup" + setupQuery;
2205
+ // Builtin cert: link directly to the app with push notification guide
2206
+ // mkcert: use HTTP onboarding server for CA install flow
2207
+ var setupUrl;
2208
+ if (config.builtinCert) {
2209
+ setupUrl = toClayStudioUrl(setupIP, config.port, "https") + "?playbook=push-notifications";
2210
+ } else if (config.tls) {
2211
+ setupUrl = "http://" + setupIP + ":" + (config.port + 1) + "/setup" + setupQuery;
2212
+ } else {
2213
+ setupUrl = "http://" + setupIP + ":" + config.port + "/setup" + setupQuery;
2214
+ }
2348
2215
  log(sym.pointer + " " + a.bold + "Continue on your device" + a.reset);
2349
2216
  log(sym.bar + " " + a.dim + "Scan the QR code or open:" + a.reset);
2350
2217
  log(sym.bar + " " + a.bold + setupUrl + a.reset);
@@ -2435,11 +2302,13 @@ function showSettingsMenu(config, ip) {
2435
2302
  { label: "Setup notifications", value: "guide" },
2436
2303
  ];
2437
2304
 
2438
- if (config.pinHash) {
2439
- items.push({ label: "Change PIN", value: "pin" });
2440
- items.push({ label: "Remove PIN", value: "remove_pin" });
2441
- } else {
2442
- items.push({ label: "Set PIN", value: "pin" });
2305
+ if (!muEnabled) {
2306
+ if (config.pinHash) {
2307
+ items.push({ label: "Change PIN", value: "pin" });
2308
+ items.push({ label: "Remove PIN", value: "remove_pin" });
2309
+ } else {
2310
+ items.push({ label: "Set PIN", value: "pin" });
2311
+ }
2443
2312
  }
2444
2313
  if (muEnabled) {
2445
2314
  items.push({ label: "Disable multi-user mode", value: "disable_multi_user" });
@@ -2759,7 +2628,9 @@ function showSettingsMenu(config, ip) {
2759
2628
  return;
2760
2629
  }
2761
2630
  var protocol = config.tls ? "https" : "http";
2762
- var recoveryUrl = protocol + "://" + ip + ":" + config.port + "/recover/" + recoveryUrlPath;
2631
+ var recoveryUrl = config.builtinCert
2632
+ ? toClayStudioUrl(ip, config.port, protocol) + "/recover/" + recoveryUrlPath
2633
+ : protocol + "://" + ip + ":" + config.port + "/recover/" + recoveryUrlPath;
2763
2634
  log(sym.bar);
2764
2635
  log(sym.bar + " " + a.yellow + sym.warn + " Admin Password Recovery" + a.reset);
2765
2636
  log(sym.bar);
@@ -2849,7 +2720,9 @@ var currentVersion = require("../package.json").version;
2849
2720
  if (headlessMode) {
2850
2721
  var protocol = config.tls ? "https" : "http";
2851
2722
  var ip = getLocalIP();
2852
- var url = protocol + "://" + ip + ":" + config.port;
2723
+ var url = config.builtinCert
2724
+ ? toClayStudioUrl(ip, config.port, protocol)
2725
+ : protocol + "://" + ip + ":" + config.port;
2853
2726
  console.log(" " + sym.done + " Daemon already running (PID " + config.pid + ")");
2854
2727
  console.log(" " + sym.done + " " + url);
2855
2728
  process.exit(0);
@@ -0,0 +1,47 @@
1
+ -----BEGIN CERTIFICATE-----
2
+ MIIDgjCCAwigAwIBAgISBmtBLGUclrEfhwU9evzgyDCQMAoGCCqGSM49BAMDMDIx
3
+ CzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQswCQYDVQQDEwJF
4
+ ODAeFw0yNjAzMjMwOTQ0MDJaFw0yNjA2MjEwOTQ0MDFaMBoxGDAWBgNVBAMMDyou
5
+ ZC5jbGF5LnN0dWRpbzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABEhAD1htqRg8
6
+ n7evflBZ1X7UaCeqBGcvG/MNtlAKd1VVfVGFuanyUjksV9++R1EuKLhEPM3loL/3
7
+ Gz8+XEewGw6jggIUMIICEDAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYIKwYB
8
+ BQUHAwEwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU8Anzwuqwtujb+h3o/CnbJu5Z
9
+ +W8wHwYDVR0jBBgwFoAUjw0TovYuftFQbDMYOF1ZjiNykcowMgYIKwYBBQUHAQEE
10
+ JjAkMCIGCCsGAQUFBzAChhZodHRwOi8vZTguaS5sZW5jci5vcmcvMBoGA1UdEQQT
11
+ MBGCDyouZC5jbGF5LnN0dWRpbzATBgNVHSAEDDAKMAgGBmeBDAECATAtBgNVHR8E
12
+ JjAkMCKgIKAehhxodHRwOi8vZTguYy5sZW5jci5vcmcvMTcuY3JsMIIBBQYKKwYB
13
+ BAHWeQIEAgSB9gSB8wDxAHcAFoMtq/CpJQ8P8DqlRf/Iv8gj0IdL9gQpJ/jnHzMT
14
+ 9foAAAGdGkoIlgAABAMASDBGAiEAhJFJwEIag1Bzt0WtYgMzLdJn/k+Is2RukdDo
15
+ G5sXpyMCIQCQOX9nOoaVIXxF1KXiavbAY5QIyJRuvK7Fn6WeL58YQQB2AMs49xWJ
16
+ fIShRF9bwd37yW7ymlnNRwppBYWwyxTDFFjnAAABnRpKCJgAAAQDAEcwRQIhAI98
17
+ cmflulGQJMfD10jbstVwodGpzl5licg6FTxcYachAiBZV1cZnPfasTzcteXyjCuz
18
+ c1wayYAtch+0soAWvBKZRzAKBggqhkjOPQQDAwNoADBlAjAwJO4ti4AJJTtMxYsr
19
+ Jf5052oDrD2POtoiPksruQVVacsq0T/9VYVX+X2vElCrxFwCMQDcSToBRWGpv/G3
20
+ JBpbEAB1qhk1Z9lYPQKH6gRvtp35XJWY0PucGRWgQUrXuZcxGlA=
21
+ -----END CERTIFICATE-----
22
+ -----BEGIN CERTIFICATE-----
23
+ MIIEVjCCAj6gAwIBAgIQY5WTY8JOcIJxWRi/w9ftVjANBgkqhkiG9w0BAQsFADBP
24
+ MQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFy
25
+ Y2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMTAeFw0yNDAzMTMwMDAwMDBa
26
+ Fw0yNzAzMTIyMzU5NTlaMDIxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBF
27
+ bmNyeXB0MQswCQYDVQQDEwJFODB2MBAGByqGSM49AgEGBSuBBAAiA2IABNFl8l7c
28
+ S7QMApzSsvru6WyrOq44ofTUOTIzxULUzDMMNMchIJBwXOhiLxxxs0LXeb5GDcHb
29
+ R6EToMffgSZjO9SNHfY9gjMy9vQr5/WWOrQTZxh7az6NSNnq3u2ubT6HTKOB+DCB
30
+ 9TAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMB
31
+ MBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFI8NE6L2Ln7RUGwzGDhdWY4j
32
+ cpHKMB8GA1UdIwQYMBaAFHm0WeZ7tuXkAXOACIjIGlj26ZtuMDIGCCsGAQUFBwEB
33
+ BCYwJDAiBggrBgEFBQcwAoYWaHR0cDovL3gxLmkubGVuY3Iub3JnLzATBgNVHSAE
34
+ DDAKMAgGBmeBDAECATAnBgNVHR8EIDAeMBygGqAYhhZodHRwOi8veDEuYy5sZW5j
35
+ ci5vcmcvMA0GCSqGSIb3DQEBCwUAA4ICAQBnE0hGINKsCYWi0Xx1ygxD5qihEjZ0
36
+ RI3tTZz1wuATH3ZwYPIp97kWEayanD1j0cDhIYzy4CkDo2jB8D5t0a6zZWzlr98d
37
+ AQFNh8uKJkIHdLShy+nUyeZxc5bNeMp1Lu0gSzE4McqfmNMvIpeiwWSYO9w82Ob8
38
+ otvXcO2JUYi3svHIWRm3+707DUbL51XMcY2iZdlCq4Wa9nbuk3WTU4gr6LY8MzVA
39
+ aDQG2+4U3eJ6qUF10bBnR1uuVyDYs9RhrwucRVnfuDj29CMLTsplM5f5wSV5hUpm
40
+ Uwp/vV7M4w4aGunt74koX71n4EdagCsL/Yk5+mAQU0+tue0JOfAV/R6t1k+Xk9s2
41
+ HMQFeoxppfzAVC04FdG9M+AC2JWxmFSt6BCuh3CEey3fE52Qrj9YM75rtvIjsm/1
42
+ Hl+u//Wqxnu1ZQ4jpa+VpuZiGOlWrqSP9eogdOhCGisnyewWJwRQOqK16wiGyZeR
43
+ xs/Bekw65vwSIaVkBruPiTfMOo0Zh4gVa8/qJgMbJbyrwwG97z/PRgmLKCDl8z3d
44
+ tA0Z7qq7fta0Gl24uyuB05dqI5J1LvAzKuWdIjT1tP8qCoxSE/xpix8hX2dt3h+/
45
+ jujUgFPFZ0EVZ0xSyBNRF3MboGZnYXFUxpNjTWPKpagDHJQmqrAcDmWJnMsFY3jS
46
+ u1igv3OefnWjSQ==
47
+ -----END CERTIFICATE-----
@@ -0,0 +1,5 @@
1
+ -----BEGIN PRIVATE KEY-----
2
+ MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgtwBpQgcr1N5kEPEv
3
+ Rz5JCBMpDQHD8U44HlzB9bLvQAihRANCAARIQA9YbakYPJ+3r35QWdV+1GgnqgRn
4
+ LxvzDbZQCndVVX1Rhbmp8lI5LFffvkdRLii4RDzN5aC/9xs/PlxHsBsO
5
+ -----END PRIVATE KEY-----
package/lib/daemon.js CHANGED
@@ -60,10 +60,26 @@ if (config.osUsers) {
60
60
  // --- TLS ---
61
61
  var tlsOptions = null;
62
62
  if (config.tls) {
63
+ // 1. Check builtin cert (shipped with package)
64
+ var builtinKeyPath = path.join(__dirname, "certs", "privkey.pem");
65
+ var builtinCertPath = path.join(__dirname, "certs", "fullchain.pem");
66
+
67
+ // 2. User cert (mkcert, etc.)
63
68
  var os = require("os");
64
69
  var certDir = path.join(process.env.CLAY_HOME || process.env.CLAUDE_RELAY_HOME || path.join(os.homedir(), ".clay"), "certs");
65
- var keyPath = path.join(certDir, "key.pem");
66
- var certPath = path.join(certDir, "cert.pem");
70
+ var userKeyPath = path.join(certDir, "key.pem");
71
+ var userCertPath = path.join(certDir, "cert.pem");
72
+
73
+ var keyPath, certPath;
74
+ if (config.builtinCert !== false && fs.existsSync(builtinKeyPath) && fs.existsSync(builtinCertPath)) {
75
+ keyPath = builtinKeyPath;
76
+ certPath = builtinCertPath;
77
+ config.builtinCert = true;
78
+ } else {
79
+ keyPath = userKeyPath;
80
+ certPath = userCertPath;
81
+ }
82
+
67
83
  try {
68
84
  tlsOptions = {
69
85
  key: fs.readFileSync(keyPath),
@@ -119,6 +135,7 @@ var listenHost = config.host || "0.0.0.0";
119
135
  var relay = createServer({
120
136
  tlsOptions: tlsOptions,
121
137
  caPath: caRoot,
138
+ builtinCert: config.builtinCert || false,
122
139
  pinHash: config.pinHash || null,
123
140
  port: config.port,
124
141
  debug: config.debug || false,
package/lib/public/app.js CHANGED
@@ -5192,6 +5192,21 @@ import { initLongPress } from './modules/longpress.js';
5192
5192
  // --- Playbook Engine ---
5193
5193
  initPlaybook();
5194
5194
 
5195
+ // Auto-open playbook from URL param (e.g. ?playbook=push-notifications)
5196
+ (function () {
5197
+ var params = new URLSearchParams(window.location.search);
5198
+ var pbId = params.get("playbook");
5199
+ if (pbId) {
5200
+ // Small delay to ensure DOM and playbook registry are ready
5201
+ setTimeout(function () { openPlaybook(pbId); }, 300);
5202
+ // Clean up URL
5203
+ params.delete("playbook");
5204
+ var clean = params.toString();
5205
+ var newUrl = window.location.pathname + (clean ? "?" + clean : "") + window.location.hash;
5206
+ window.history.replaceState(null, "", newUrl);
5207
+ }
5208
+ })();
5209
+
5195
5210
  // --- In-session search (Cmd+F / Ctrl+F) ---
5196
5211
  initSessionSearch({
5197
5212
  messagesEl: messagesEl,
@@ -6751,7 +6766,13 @@ import { initLongPress } from './modules/longpress.js';
6751
6766
  modal.querySelector(".pwa-modal-backdrop").addEventListener("click", closeModal);
6752
6767
 
6753
6768
  confirmBtn.addEventListener("click", function () {
6754
- // Redirect to setup page
6769
+ // Builtin cert (*.d.clay.studio): open push notification guide directly
6770
+ if (location.hostname.endsWith(".d.clay.studio")) {
6771
+ closeModal();
6772
+ openPlaybook("push-notifications");
6773
+ return;
6774
+ }
6775
+ // mkcert / other: redirect to onboarding setup page
6755
6776
  var port = parseInt(location.port, 10);
6756
6777
  var setupUrl;
6757
6778
  if (!port) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "2.16.0",
3
+ "version": "2.17.0-beta.1",
4
4
  "description": "Web UI for Claude Code. Any device. Push notifications.",
5
5
  "bin": {
6
6
  "clay-server": "./bin/cli.js",